fix(settings): sidebar_visible/width 영속화 — IPC + store hydration 추가
final code review 의 Important issue 대응. SettingsService 의 setSidebarVisible/ setSidebarWidth getter/setter 는 이미 있었지만 IPC handler + store hydration missing 으로 매 launch 시 사이드바 닫힌 상태로 시작하던 회귀. - settings:set-sidebar-visible / set-sidebar-width IPC 핸들러 추가 - InboxApi.getSettings 응답에 sidebar_visible/sidebar_width 포함 - preload 의 setSidebarVisible/setSidebarWidth invoke 노출 - store.loadInitial 가 settings.sidebar_visible/sidebar_width 로 hydrate - store.toggleSidebar 가 IPC 호출하여 영속화 - test mock 에 setSidebarVisible/setSidebarWidth 추가 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,17 @@ export function registerSettingsApi(deps?: SettingsIpcDeps): void {
|
|||||||
return { ok: true as const };
|
return { ok: true as const };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// v0.4 — Sidebar UI state 영속화
|
||||||
|
ipcMain.handle('settings:set-sidebar-visible', async (_e, visible: boolean) => {
|
||||||
|
await settings.setSidebarVisible(visible);
|
||||||
|
return { ok: true as const };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('settings:set-sidebar-width', async (_e, width: number) => {
|
||||||
|
await settings.setSidebarWidth(width);
|
||||||
|
return { ok: true as const };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('settings:set-sync-auto-enabled', async (_e, value: boolean) => {
|
ipcMain.handle('settings:set-sync-auto-enabled', async (_e, value: boolean) => {
|
||||||
await deps.settings.setAutoSyncEnabled(value);
|
await deps.settings.setAutoSyncEnabled(value);
|
||||||
await deps.syncTimer?.reconfigure();
|
await deps.syncTimer?.reconfigure();
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ const api: InklingApi = {
|
|||||||
getSettings: () => ipcRenderer.invoke('settings:get'),
|
getSettings: () => ipcRenderer.invoke('settings:get'),
|
||||||
setAiEnabled: (enabled: boolean) => ipcRenderer.invoke('settings:set-ai-enabled', enabled),
|
setAiEnabled: (enabled: boolean) => ipcRenderer.invoke('settings:set-ai-enabled', enabled),
|
||||||
setOnboardingCompleted: (completed: boolean) => ipcRenderer.invoke('settings:set-onboarding-completed', completed),
|
setOnboardingCompleted: (completed: boolean) => ipcRenderer.invoke('settings:set-onboarding-completed', completed),
|
||||||
|
// v0.4 — Sidebar UI state 영속화
|
||||||
|
setSidebarVisible: (visible: boolean) => ipcRenderer.invoke('settings:set-sidebar-visible', visible),
|
||||||
|
setSidebarWidth: (width: number) => ipcRenderer.invoke('settings:set-sidebar-width', width),
|
||||||
// v0.2.9 Cut B Task 16 — disabled 메모 재투입 + count.
|
// v0.2.9 Cut B Task 16 — disabled 메모 재투입 + count.
|
||||||
enqueueDisabled: () => ipcRenderer.invoke('inbox:enqueue-disabled'),
|
enqueueDisabled: () => ipcRenderer.invoke('inbox:enqueue-disabled'),
|
||||||
getDisabledCount: () => ipcRenderer.invoke('inbox:get-disabled-count'),
|
getDisabledCount: () => ipcRenderer.invoke('inbox:get-disabled-count'),
|
||||||
|
|||||||
@@ -152,7 +152,14 @@ export const useInbox = create<InboxState>((set, get) => ({
|
|||||||
inboxApi.countsByStatus({ notebookId }),
|
inboxApi.countsByStatus({ notebookId }),
|
||||||
inboxApi.getSettings()
|
inboxApi.getSettings()
|
||||||
]);
|
]);
|
||||||
set({ notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates, failedCount, recallCandidate, counts, ai_enabled: settings.ai_enabled ?? true, loading: false });
|
set({
|
||||||
|
notes, continuity, pendingCount, ollamaStatus, todayCount, trashCount, expiredCandidates,
|
||||||
|
failedCount, recallCandidate, counts,
|
||||||
|
ai_enabled: settings.ai_enabled ?? true,
|
||||||
|
sidebarVisible: settings.sidebar_visible ?? false,
|
||||||
|
sidebarWidth: settings.sidebar_width ?? 240,
|
||||||
|
loading: false
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 첫 launch 의 IPC 실패 (DB migration 실패 / main process 비정상) 시 무한 loading 회피.
|
// 첫 launch 의 IPC 실패 (DB migration 실패 / main process 비정상) 시 무한 loading 회피.
|
||||||
// 빈 데이터로 진입하면 사용자가 캡처 시도 → 실제 fail 이 표면화 → 재시도 가능.
|
// 빈 데이터로 진입하면 사용자가 캡처 시도 → 실제 fail 이 표면화 → 재시도 가능.
|
||||||
@@ -480,7 +487,9 @@ export const useInbox = create<InboxState>((set, get) => ({
|
|||||||
await get().refreshMeta();
|
await get().refreshMeta();
|
||||||
},
|
},
|
||||||
toggleSidebar() {
|
toggleSidebar() {
|
||||||
set({ sidebarVisible: !get().sidebarVisible });
|
const next = !get().sidebarVisible;
|
||||||
|
set({ sidebarVisible: next });
|
||||||
|
void inboxApi.setSidebarVisible(next);
|
||||||
},
|
},
|
||||||
// v0.4 Task 11 — promotion candidate actions.
|
// v0.4 Task 11 — promotion candidate actions.
|
||||||
async loadPromotionCandidates() {
|
async loadPromotionCandidates() {
|
||||||
|
|||||||
@@ -228,8 +228,13 @@ export interface InboxApi {
|
|||||||
vision_model?: string | null;
|
vision_model?: string | null;
|
||||||
vision_capable_cache?: string[];
|
vision_capable_cache?: string[];
|
||||||
vision_cache_at?: string;
|
vision_cache_at?: string;
|
||||||
|
// v0.4 — Sidebar UI state 영속화
|
||||||
|
sidebar_visible?: boolean;
|
||||||
|
sidebar_width?: number;
|
||||||
}>;
|
}>;
|
||||||
setAiEnabled(enabled: boolean): Promise<{ ok: true }>;
|
setAiEnabled(enabled: boolean): Promise<{ ok: true }>;
|
||||||
|
setSidebarVisible(visible: boolean): Promise<{ ok: true }>;
|
||||||
|
setSidebarWidth(width: number): Promise<{ ok: true }>;
|
||||||
setOnboardingCompleted(completed: boolean): Promise<{ ok: true }>;
|
setOnboardingCompleted(completed: boolean): Promise<{ ok: true }>;
|
||||||
// v0.2.9 Cut B Task 16 — ai_status='disabled' 메모 재투입 (사용자가 ai_enabled OFF→ON 전환 시).
|
// v0.2.9 Cut B Task 16 — ai_status='disabled' 메모 재투입 (사용자가 ai_enabled OFF→ON 전환 시).
|
||||||
enqueueDisabled(): Promise<{ count: number }>;
|
enqueueDisabled(): Promise<{ count: number }>;
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({
|
|||||||
// v0.2.9 Cut B Task 12 — onboarding wizard 분기. default 는 onboarding_completed=true 라 wizard 미표시.
|
// v0.2.9 Cut B Task 12 — onboarding wizard 분기. default 는 onboarding_completed=true 라 wizard 미표시.
|
||||||
getSettings: vi.fn(async () => ({ onboarding_completed: true })),
|
getSettings: vi.fn(async () => ({ onboarding_completed: true })),
|
||||||
setAiEnabled: vi.fn(async () => ({ ok: true as const })),
|
setAiEnabled: vi.fn(async () => ({ ok: true as const })),
|
||||||
|
setSidebarVisible: vi.fn(async () => ({ ok: true as const })),
|
||||||
|
setSidebarWidth: vi.fn(async () => ({ ok: true as const })),
|
||||||
setOnboardingCompleted: vi.fn(async () => ({ ok: true as const })),
|
setOnboardingCompleted: vi.fn(async () => ({ ok: true as const })),
|
||||||
// v0.2.9 Cut B Task 16 — AiProviderSection 가 SettingsPage 렌더 시 호출.
|
// v0.2.9 Cut B Task 16 — AiProviderSection 가 SettingsPage 렌더 시 호출.
|
||||||
getDisabledCount: vi.fn(async () => 0),
|
getDisabledCount: vi.fn(async () => 0),
|
||||||
@@ -174,11 +176,12 @@ describe('App header — 3 tabs (v0.4)', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Sidebar 컴포넌트가 렌더 트리에 포함됨 (sidebarVisible=true)', async () => {
|
it('Sidebar 컴포넌트가 렌더 트리에 포함됨 (sidebarVisible=true)', async () => {
|
||||||
useInbox.setState({ sidebarVisible: true, notebooks: [] });
|
// loadInitial 의 getSettings 가 sidebar_visible=true 반환 (Strict Mode 중복 호출 대비 mockResolvedValue).
|
||||||
|
vi.mocked(inboxApi.getSettings).mockResolvedValue({ onboarding_completed: true, sidebar_visible: true });
|
||||||
render(<App />);
|
render(<App />);
|
||||||
await screen.findByRole('tab', { name: /Inbox/ });
|
await screen.findByRole('tab', { name: /Inbox/ });
|
||||||
// Sidebar renders an <aside> element when visible
|
// loadInitial 비동기 hydrate 가 완료될 때까지 기다림
|
||||||
expect(document.querySelector('aside')).not.toBeNull();
|
await waitFor(() => expect(document.querySelector('aside')).not.toBeNull());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ vi.mock('../../src/renderer/inbox/api.js', () => ({
|
|||||||
listRecallCandidate: vi.fn(async () => null),
|
listRecallCandidate: vi.fn(async () => null),
|
||||||
countsByStatus: mockCountsByStatus,
|
countsByStatus: mockCountsByStatus,
|
||||||
getSettings: vi.fn(async () => ({ ai_enabled: true })),
|
getSettings: vi.fn(async () => ({ ai_enabled: true })),
|
||||||
listByStatus: mockListByStatus
|
listByStatus: mockListByStatus,
|
||||||
|
setSidebarVisible: vi.fn(async () => ({ ok: true as const })),
|
||||||
|
setSidebarWidth: vi.fn(async () => ({ ok: true as const }))
|
||||||
},
|
},
|
||||||
notebookApi: {
|
notebookApi: {
|
||||||
list: vi.fn(async () => [
|
list: vi.fn(async () => [
|
||||||
|
|||||||
Reference in New Issue
Block a user