From 56873f69363f503e778dc0178f56dfe847598630 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 4 Jul 2025 15:52:17 +1000 Subject: [PATCH] feat(ui): queue and models tab are wrapped in dockview panels --- .../src/features/ui/components/AppContent.tsx | 9 +- .../ui/layouts/models-tab-auto-layout.tsx | 64 ++++++ .../ui/layouts/navigation-api.test.ts | 211 ++++++++---------- .../src/features/ui/layouts/navigation-api.ts | 30 +-- .../ui/layouts/queue-tab-auto-layout.tsx | 64 ++++++ .../web/src/features/ui/layouts/shared.ts | 3 + 6 files changed, 238 insertions(+), 143 deletions(-) create mode 100644 invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx create mode 100644 invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx index 2e67c94b2f..4300aef499 100644 --- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx @@ -16,14 +16,13 @@ import { import { VerticalNavBar } from 'features/ui/components/VerticalNavBar'; import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout'; import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout'; +import { ModelsTabAutoLayout } from 'features/ui/layouts/models-tab-auto-layout'; +import { QueueTabAutoLayout } from 'features/ui/layouts/queue-tab-auto-layout'; import { UpscalingTabAutoLayout } from 'features/ui/layouts/upscaling-tab-auto-layout'; import { WorkflowsTabAutoLayout } from 'features/ui/layouts/workflows-tab-auto-layout'; import { selectActiveTab } from 'features/ui/store/uiSelectors'; import { memo, useState } from 'react'; -import ModelManagerTab from './tabs/ModelManagerTab'; -import QueueTab from './tabs/QueueTab'; - export const AppContent = memo(() => { useDndMonitor(); const tab = useAppSelector(selectActiveTab); @@ -43,8 +42,8 @@ export const AppContent = memo(() => { {withCanvasTab && tab === 'canvas' && } {withUpscalingTab && tab === 'upscaling' && } {withWorkflowsTab && tab === 'workflows' && } - {withModelsTab && tab === 'models' && } - {withQueueTab && tab === 'queue' && } + {withModelsTab && tab === 'models' && } + {withQueueTab && tab === 'queue' && } {isLoading && } diff --git a/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx new file mode 100644 index 0000000000..f45edcd486 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx @@ -0,0 +1,64 @@ +import type { GridviewApi, IGridviewPanel, IGridviewReactProps } from 'dockview'; +import { GridviewReact, LayoutPriority, Orientation } from 'dockview'; +import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab'; +import type { RootLayoutGridviewComponents } from 'features/ui/layouts/auto-layout-context'; +import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; + +import { navigationApi } from './navigation-api'; +import { MODELS_PANEL_ID } from './shared'; + +export const rootPanelComponents: RootLayoutGridviewComponents = { + [MODELS_PANEL_ID]: ModelManagerTab, +}; + +export const initializeRootPanelLayout = (layoutApi: GridviewApi) => { + const models = layoutApi.addPanel({ + id: MODELS_PANEL_ID, + component: MODELS_PANEL_ID, + priority: LayoutPriority.High, + }); + + navigationApi.registerPanel('models', MODELS_PANEL_ID, models); + + return { models } satisfies Record; +}; + +export const ModelsTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => { + const rootRef = useRef(null); + const [rootApi, setRootApi] = useState(null); + const onReady = useCallback(({ api }) => { + setRootApi(api); + }, []); + + useEffect(() => { + setIsLoading(true); + + if (!rootApi) { + return; + } + + initializeRootPanelLayout(rootApi); + + setTimeout(() => { + setIsLoading(false); + }, 300); + + return () => { + navigationApi.unregisterTab('models'); + }; + }, [rootApi, setIsLoading]); + + return ( + + + + ); +}); +ModelsTabAutoLayout.displayName = 'ModelsTabAutoLayout'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.test.ts b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.test.ts index dde2d147c3..5ce399a1c8 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.test.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.test.ts @@ -2,7 +2,15 @@ import { GridviewPanel, type IDockviewPanel } from 'dockview'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { NavigationApi } from './navigation-api'; -import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from './shared'; +import { + LAUNCHPAD_PANEL_ID, + LEFT_PANEL_ID, + LEFT_PANEL_MIN_SIZE_PX, + RIGHT_PANEL_ID, + RIGHT_PANEL_MIN_SIZE_PX, + SETTINGS_PANEL_ID, + WORKSPACE_PANEL_ID, +} from './shared'; // Mock the logger vi.mock('app/logging/logger', () => ({ @@ -91,24 +99,24 @@ describe('AppNavigationApi', () => { describe('Panel Registration', () => { it('should register and unregister panels', () => { const mockPanel = createMockPanel(); - const unregister = navigationApi.registerPanel('generate', 'settings', mockPanel); + const unregister = navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); expect(typeof unregister).toBe('function'); - expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true); + expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true); // Unregister unregister(); - expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(false); + expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(false); }); it('should resolve waiting promises when panel is registered', async () => { const mockPanel = createMockPanel(); // Start waiting for registration - const waitPromise = navigationApi.waitForPanel('generate', 'settings'); + const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID); // Register the panel - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); // Wait should resolve await expect(waitPromise).resolves.toBeUndefined(); @@ -118,30 +126,30 @@ describe('AppNavigationApi', () => { const mockPanel1 = createMockPanel(); const mockPanel2 = createMockDockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel1); - navigationApi.registerPanel('generate', 'launchpad', mockPanel2); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1); + navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2); - expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true); - expect(navigationApi.isPanelRegistered('generate', 'launchpad')).toBe(true); + expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true); + expect(navigationApi.isPanelRegistered('generate', LAUNCHPAD_PANEL_ID)).toBe(true); const registeredPanels = navigationApi.getRegisteredPanels('generate'); - expect(registeredPanels).toContain('settings'); - expect(registeredPanels).toContain('launchpad'); + expect(registeredPanels).toContain(SETTINGS_PANEL_ID); + expect(registeredPanels).toContain(LAUNCHPAD_PANEL_ID); }); it('should handle panels across different tabs', () => { const mockPanel1 = createMockPanel(); const mockPanel2 = createMockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel1); - navigationApi.registerPanel('canvas', 'settings', mockPanel2); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1); + navigationApi.registerPanel('canvas', SETTINGS_PANEL_ID, mockPanel2); - expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true); - expect(navigationApi.isPanelRegistered('canvas', 'settings')).toBe(true); + expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true); + expect(navigationApi.isPanelRegistered('canvas', SETTINGS_PANEL_ID)).toBe(true); // Same panel ID in different tabs should be separate - expect(navigationApi.getRegisteredPanels('generate')).toEqual(['settings']); - expect(navigationApi.getRegisteredPanels('canvas')).toEqual(['settings']); + expect(navigationApi.getRegisteredPanels('generate')).toEqual([SETTINGS_PANEL_ID]); + expect(navigationApi.getRegisteredPanels('canvas')).toEqual([SETTINGS_PANEL_ID]); }); }); @@ -152,10 +160,10 @@ describe('AppNavigationApi', () => { it('should focus panel in already registered tab', async () => { const mockPanel = createMockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); - const result = await navigationApi.focusPanel('generate', 'settings'); + const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); expect(result).toBe(true); expect(mockSetAppTab).not.toHaveBeenCalled(); @@ -164,10 +172,10 @@ describe('AppNavigationApi', () => { it('should switch tab before focusing panel', async () => { const mockPanel = createMockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('canvas'); // Currently on different tab - const result = await navigationApi.focusPanel('generate', 'settings'); + const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); expect(result).toBe(true); expect(mockSetAppTab).toHaveBeenCalledWith('generate'); @@ -179,11 +187,11 @@ describe('AppNavigationApi', () => { mockGetAppTab.mockReturnValue('generate'); // Start focus operation before panel is registered - const focusPromise = navigationApi.focusPanel('generate', 'settings'); + const focusPromise = navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); // Register panel after a short delay setTimeout(() => { - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); }, 100); const result = await focusPromise; @@ -196,17 +204,17 @@ describe('AppNavigationApi', () => { const mockGridPanel = createMockPanel(); const mockDockPanel = createMockDockPanel(); - navigationApi.registerPanel('generate', 'settings', mockGridPanel); - navigationApi.registerPanel('generate', 'launchpad', mockDockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockGridPanel); + navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockDockPanel); mockGetAppTab.mockReturnValue('generate'); // Test gridview panel - const result1 = await navigationApi.focusPanel('generate', 'settings'); + const result1 = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); expect(result1).toBe(true); expect(mockGridPanel.api.setActive).toHaveBeenCalledOnce(); // Test dockview panel - const result2 = await navigationApi.focusPanel('generate', 'launchpad'); + const result2 = await navigationApi.focusPanel('generate', LAUNCHPAD_PANEL_ID); expect(result2).toBe(true); expect(mockDockPanel.api.setActive).toHaveBeenCalledOnce(); }); @@ -215,7 +223,7 @@ describe('AppNavigationApi', () => { mockGetAppTab.mockReturnValue('generate'); // Set a short timeout for testing - const result = await navigationApi.focusPanel('generate', 'settings'); + const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); expect(result).toBe(false); }); @@ -228,20 +236,20 @@ describe('AppNavigationApi', () => { throw new Error('Mock error'); }); - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); - const result = await navigationApi.focusPanel('generate', 'settings'); + const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); expect(result).toBe(false); }); it('should work without app connection', async () => { const mockPanel = createMockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); // Don't connect to app - const result = await navigationApi.focusPanel('generate', 'settings'); + const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); expect(result).toBe(true); expect(mockPanel.api.setActive).toHaveBeenCalledOnce(); @@ -251,9 +259,9 @@ describe('AppNavigationApi', () => { describe('Panel Waiting', () => { it('should resolve immediately for already registered panels', async () => { const mockPanel = createMockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); - const waitPromise = navigationApi.waitForPanel('generate', 'settings'); + const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID); await expect(waitPromise).resolves.toBeUndefined(); }); @@ -261,25 +269,25 @@ describe('AppNavigationApi', () => { it('should handle multiple waiters for same panel', async () => { const mockPanel = createMockPanel(); - const waitPromise1 = navigationApi.waitForPanel('generate', 'settings'); - const waitPromise2 = navigationApi.waitForPanel('generate', 'settings'); + const waitPromise1 = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID); + const waitPromise2 = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID); setTimeout(() => { - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); }, 50); await expect(Promise.all([waitPromise1, waitPromise2])).resolves.toEqual([undefined, undefined]); }); it('should timeout if panel is not registered', async () => { - const waitPromise = navigationApi.waitForPanel('generate', 'settings', 100); + const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID, 100); await expect(waitPromise).rejects.toThrow('Panel generate:settings registration timed out after 100ms'); }); it('should handle custom timeout', async () => { const start = Date.now(); - const waitPromise = navigationApi.waitForPanel('generate', 'settings', 200); + const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID, 200); await expect(waitPromise).rejects.toThrow('Panel generate:settings registration timed out after 200ms'); @@ -295,9 +303,9 @@ describe('AppNavigationApi', () => { const mockPanel2 = createMockPanel(); const mockPanel3 = createMockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel1); - navigationApi.registerPanel('generate', 'launchpad', mockPanel2); - navigationApi.registerPanel('canvas', 'settings', mockPanel3); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1); + navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2); + navigationApi.registerPanel('canvas', SETTINGS_PANEL_ID, mockPanel3); expect(navigationApi.getRegisteredPanels('generate')).toHaveLength(2); expect(navigationApi.getRegisteredPanels('canvas')).toHaveLength(1); @@ -309,7 +317,7 @@ describe('AppNavigationApi', () => { }); it('should clean up pending promises when unregistering tab', async () => { - const waitPromise = navigationApi.waitForPanel('generate', 'settings', 5000); + const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID, 5000); navigationApi.unregisterTab('generate'); @@ -327,10 +335,10 @@ describe('AppNavigationApi', () => { mockGetAppTab.mockReturnValue('canvas'); // Register panel - const unregister = navigationApi.registerPanel('generate', 'settings', mockPanel); + const unregister = navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); // Focus panel (should switch tab and focus) - const result = await navigationApi.focusPanel('generate', 'settings'); + const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); expect(result).toBe(true); expect(mockSetAppTab).toHaveBeenCalledWith('generate'); @@ -342,7 +350,7 @@ describe('AppNavigationApi', () => { expect(navigationApi.setAppTab).toBeNull(); expect(navigationApi.getAppTab).toBeNull(); - expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(false); + expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(false); }); it('should handle multiple panels and tabs', async () => { @@ -354,19 +362,19 @@ describe('AppNavigationApi', () => { mockGetAppTab.mockReturnValue('generate'); // Register panels - navigationApi.registerPanel('generate', 'settings', mockPanel1); - navigationApi.registerPanel('generate', 'launchpad', mockPanel2); - navigationApi.registerPanel('canvas', 'workspace', mockPanel3); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1); + navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2); + navigationApi.registerPanel('canvas', WORKSPACE_PANEL_ID, mockPanel3); // Focus panels - await navigationApi.focusPanel('generate', 'settings'); + await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); expect(mockPanel1.api.setActive).toHaveBeenCalledOnce(); - await navigationApi.focusPanel('generate', 'launchpad'); + await navigationApi.focusPanel('generate', LAUNCHPAD_PANEL_ID); expect(mockPanel2.api.setActive).toHaveBeenCalledOnce(); mockGetAppTab.mockReturnValue('generate'); - await navigationApi.focusPanel('canvas', 'workspace'); + await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID); expect(mockSetAppTab).toHaveBeenCalledWith('canvas'); expect(mockPanel3.api.setActive).toHaveBeenCalledOnce(); }); @@ -376,11 +384,11 @@ describe('AppNavigationApi', () => { mockGetAppTab.mockReturnValue('generate'); // Start focusing before registration - const focusPromise = navigationApi.focusPanel('generate', 'settings'); + const focusPromise = navigationApi.focusPanel('generate', SETTINGS_PANEL_ID); // Register after delay setTimeout(() => { - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); }, 50); const result = await focusPromise; @@ -390,33 +398,6 @@ describe('AppNavigationApi', () => { }); }); - describe('Tab Validation', () => { - it('should reject non-enabled tabs in waitForPanel', async () => { - await expect(navigationApi.waitForPanel('models', 'settings')).rejects.toThrow( - 'Tab models is not enabled for panel registration' - ); - }); - - it('should reject non-enabled tabs in focusPanel', async () => { - const result = await navigationApi.focusPanel('models', 'settings'); - expect(result).toBe(false); - }); - - it('should warn for non-enabled tabs in getPanel', () => { - const result = navigationApi.getPanel('models', 'settings'); - expect(result).toBeUndefined(); - }); - - it('should accept all enabled tabs', async () => { - const enabledTabs = ['canvas', 'generate', 'workflows', 'upscaling'] as const; - - for (const tab of enabledTabs) { - // These should timeout since panels aren't registered, but not reject due to invalid tab - await expect(navigationApi.waitForPanel(tab, 'settings', 50)).rejects.toThrow('timed out'); - } - }); - }); - describe('focusPanelInActiveTab', () => { beforeEach(() => { navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab }); @@ -424,10 +405,10 @@ describe('AppNavigationApi', () => { it('should focus panel in active tab', async () => { const mockPanel = createMockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); - const result = await navigationApi.focusPanelInActiveTab('settings'); + const result = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID); expect(result).toBe(true); expect(mockPanel.api.setActive).toHaveBeenCalledOnce(); @@ -436,7 +417,7 @@ describe('AppNavigationApi', () => { it('should return false when no active tab', async () => { mockGetAppTab.mockReturnValue(null); - const result = await navigationApi.focusPanelInActiveTab('settings'); + const result = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID); expect(result).toBe(false); }); @@ -444,7 +425,7 @@ describe('AppNavigationApi', () => { it('should work without app connection', async () => { navigationApi.disconnectFromApp(); - const result = await navigationApi.focusPanelInActiveTab('settings'); + const result = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID); expect(result).toBe(false); }); @@ -480,9 +461,9 @@ describe('AppNavigationApi', () => { describe('getPanel', () => { it('should return registered panel', () => { const mockPanel = createMockPanel(); - navigationApi.registerPanel('generate', 'settings', mockPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel); - const result = navigationApi.getPanel('generate', 'settings'); + const result = navigationApi.getPanel('generate', SETTINGS_PANEL_ID); expect(result).toBe(mockPanel); }); @@ -494,7 +475,7 @@ describe('AppNavigationApi', () => { }); it('should return undefined for non-enabled tab', () => { - const result = navigationApi.getPanel('models', 'settings'); + const result = navigationApi.getPanel('models', SETTINGS_PANEL_ID); expect(result).toBeUndefined(); }); @@ -507,7 +488,7 @@ describe('AppNavigationApi', () => { it('should expand collapsed left panel', () => { const mockPanel = createMockPanel({ maximumWidth: 0 }); - navigationApi.registerPanel('generate', 'left', mockPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleLeftPanel(); @@ -522,7 +503,7 @@ describe('AppNavigationApi', () => { it('should collapse expanded left panel', () => { const mockPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER }); - navigationApi.registerPanel('generate', 'left', mockPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleLeftPanel(); @@ -553,7 +534,7 @@ describe('AppNavigationApi', () => { it('should return false when panel is not GridviewPanel', () => { const mockPanel = createMockDockPanel(); - navigationApi.registerPanel('generate', 'left', mockPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleLeftPanel(); @@ -569,7 +550,7 @@ describe('AppNavigationApi', () => { it('should expand collapsed right panel', () => { const mockPanel = createMockPanel({ maximumWidth: 0 }); - navigationApi.registerPanel('generate', 'right', mockPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleRightPanel(); @@ -584,7 +565,7 @@ describe('AppNavigationApi', () => { it('should collapse expanded right panel', () => { const mockPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER }); - navigationApi.registerPanel('generate', 'right', mockPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleRightPanel(); @@ -615,7 +596,7 @@ describe('AppNavigationApi', () => { it('should return false when panel is not GridviewPanel', () => { const mockPanel = createMockDockPanel(); - navigationApi.registerPanel('generate', 'right', mockPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleRightPanel(); @@ -633,8 +614,8 @@ describe('AppNavigationApi', () => { const leftPanel = createMockPanel({ maximumWidth: 0 }); const rightPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER }); - navigationApi.registerPanel('generate', 'left', leftPanel); - navigationApi.registerPanel('generate', 'right', rightPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleLeftAndRightPanels(); @@ -656,8 +637,8 @@ describe('AppNavigationApi', () => { const leftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER }); const rightPanel = createMockPanel({ maximumWidth: 0 }); - navigationApi.registerPanel('generate', 'left', leftPanel); - navigationApi.registerPanel('generate', 'right', rightPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleLeftAndRightPanels(); @@ -679,8 +660,8 @@ describe('AppNavigationApi', () => { const leftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER }); const rightPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER }); - navigationApi.registerPanel('generate', 'left', leftPanel); - navigationApi.registerPanel('generate', 'right', rightPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleLeftAndRightPanels(); @@ -702,8 +683,8 @@ describe('AppNavigationApi', () => { const leftPanel = createMockPanel({ maximumWidth: 0 }); const rightPanel = createMockPanel({ maximumWidth: 0 }); - navigationApi.registerPanel('generate', 'left', leftPanel); - navigationApi.registerPanel('generate', 'right', rightPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleLeftAndRightPanels(); @@ -741,8 +722,8 @@ describe('AppNavigationApi', () => { const leftPanel = createMockDockPanel(); const rightPanel = createMockDockPanel(); - navigationApi.registerPanel('generate', 'left', leftPanel); - navigationApi.registerPanel('generate', 'right', rightPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.toggleLeftAndRightPanels(); @@ -760,8 +741,8 @@ describe('AppNavigationApi', () => { const leftPanel = createMockPanel({ maximumWidth: 0 }); const rightPanel = createMockPanel({ maximumWidth: 0 }); - navigationApi.registerPanel('generate', 'left', leftPanel); - navigationApi.registerPanel('generate', 'right', rightPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.resetLeftAndRightPanels(); @@ -802,8 +783,8 @@ describe('AppNavigationApi', () => { const leftPanel = createMockDockPanel(); const rightPanel = createMockDockPanel(); - navigationApi.registerPanel('generate', 'left', leftPanel); - navigationApi.registerPanel('generate', 'right', rightPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel); mockGetAppTab.mockReturnValue('generate'); const result = navigationApi.resetLeftAndRightPanels(); @@ -823,13 +804,13 @@ describe('AppNavigationApi', () => { const settingsPanel = createMockPanel(); // Register panels - navigationApi.registerPanel('generate', 'left', leftPanel); - navigationApi.registerPanel('generate', 'right', rightPanel); - navigationApi.registerPanel('generate', 'settings', settingsPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel); + navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel); + navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, settingsPanel); mockGetAppTab.mockReturnValue('generate'); // Focus a panel in active tab - const focusResult = await navigationApi.focusPanelInActiveTab('settings'); + const focusResult = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID); expect(focusResult).toBe(true); expect(settingsPanel.api.setActive).toHaveBeenCalled(); @@ -854,8 +835,8 @@ describe('AppNavigationApi', () => { const generateLeftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER }); const canvasLeftPanel = createMockPanel({ maximumWidth: 0 }); - navigationApi.registerPanel('generate', 'left', generateLeftPanel); - navigationApi.registerPanel('canvas', 'left', canvasLeftPanel); + navigationApi.registerPanel('generate', LEFT_PANEL_ID, generateLeftPanel); + navigationApi.registerPanel('canvas', LEFT_PANEL_ID, canvasLeftPanel); // Start on generate tab mockGetAppTab.mockReturnValue('generate'); diff --git a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts index 2cc6aedfb2..6303b10cb6 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts @@ -3,7 +3,7 @@ import { createDeferredPromise, type Deferred } from 'common/util/createDeferred import { GridviewPanel, type IDockviewPanel, type IGridviewPanel } from 'dockview'; import type { TabName } from 'features/ui/store/uiTypes'; -import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from './shared'; +import { LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX } from './shared'; const log = logger('system'); @@ -14,8 +14,6 @@ type Waiter = { timeoutId: ReturnType | null; }; -const PANEL_ENABLED_TABS: TabName[] = ['canvas', 'generate', 'workflows', 'upscaling']; - export class NavigationApi { private panels: Map = new Map(); private waiters: Map = new Map(); @@ -75,11 +73,6 @@ export class NavigationApi { * @returns Promise that resolves when the panel is ready */ waitForPanel = (tab: TabName, panelId: string, timeout = 2000): Promise => { - if (!PANEL_ENABLED_TABS.includes(tab)) { - log.error(`Tab ${tab} is not enabled for panel registration`); - return Promise.reject(new Error(`Tab ${tab} is not enabled for panel registration`)); - } - const key = this.getPanelKey(tab, panelId); if (this.panels.has(key)) { @@ -123,11 +116,6 @@ export class NavigationApi { * @returns Promise that resolves to true if successful, false otherwise */ focusPanel = async (tab: TabName, panelId: string): Promise => { - if (!PANEL_ENABLED_TABS.includes(tab)) { - log.error(`Tab ${tab} is not enabled for panel registration`); - return Promise.resolve(false); - } - try { // Switch to the target tab if needed if (this.setAppTab && this.getAppTab && this.getAppTab() !== tab) { @@ -176,10 +164,6 @@ export class NavigationApi { }; getPanel = (tab: TabName, panelId: string): PanelType | undefined => { - if (!PANEL_ENABLED_TABS.includes(tab)) { - log.warn(`Tab ${tab} is not enabled for panel registration`); - return undefined; - } const key = this.getPanelKey(tab, panelId); return this.panels.get(key); }; @@ -190,7 +174,7 @@ export class NavigationApi { log.warn('No active tab found to toggle left panel'); return false; } - const leftPanel = this.getPanel(activeTab, 'left'); + const leftPanel = this.getPanel(activeTab, LEFT_PANEL_ID); if (!leftPanel) { log.warn(`Left panel not found in active tab "${activeTab}"`); return false; @@ -216,7 +200,7 @@ export class NavigationApi { log.warn('No active tab found to toggle right panel'); return false; } - const rightPanel = this.getPanel(activeTab, 'right'); + const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID); if (!rightPanel) { log.warn(`Right panel not found in active tab "${activeTab}"`); return false; @@ -242,8 +226,8 @@ export class NavigationApi { log.warn('No active tab found to toggle right panel'); return false; } - const leftPanel = this.getPanel(activeTab, 'left'); - const rightPanel = this.getPanel(activeTab, 'right'); + const leftPanel = this.getPanel(activeTab, LEFT_PANEL_ID); + const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID); if (!rightPanel || !leftPanel) { log.warn(`Right and/or left panel not found in tab "${activeTab}"`); @@ -277,8 +261,8 @@ export class NavigationApi { log.warn('No active tab found to toggle right panel'); return false; } - const leftPanel = this.getPanel(activeTab, 'left'); - const rightPanel = this.getPanel(activeTab, 'right'); + const leftPanel = this.getPanel(activeTab, LEFT_PANEL_ID); + const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID); if (!rightPanel || !leftPanel) { log.warn(`Right and/or left panel not found in tab "${activeTab}"`); diff --git a/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx new file mode 100644 index 0000000000..7820aee9ab --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx @@ -0,0 +1,64 @@ +import type { GridviewApi, IGridviewPanel, IGridviewReactProps } from 'dockview'; +import { GridviewReact, LayoutPriority, Orientation } from 'dockview'; +import QueueTab from 'features/ui/components/tabs/QueueTab'; +import type { RootLayoutGridviewComponents } from 'features/ui/layouts/auto-layout-context'; +import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; + +import { navigationApi } from './navigation-api'; +import { QUEUE_PANEL_ID } from './shared'; + +export const rootPanelComponents: RootLayoutGridviewComponents = { + [QUEUE_PANEL_ID]: QueueTab, +}; + +export const initializeRootPanelLayout = (layoutApi: GridviewApi) => { + const queue = layoutApi.addPanel({ + id: QUEUE_PANEL_ID, + component: QUEUE_PANEL_ID, + priority: LayoutPriority.High, + }); + + navigationApi.registerPanel('queue', QUEUE_PANEL_ID, queue); + + return { queue } satisfies Record; +}; + +export const QueueTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => { + const rootRef = useRef(null); + const [rootApi, setRootApi] = useState(null); + const onReady = useCallback(({ api }) => { + setRootApi(api); + }, []); + + useEffect(() => { + setIsLoading(true); + + if (!rootApi) { + return; + } + + initializeRootPanelLayout(rootApi); + + setTimeout(() => { + setIsLoading(false); + }, 300); + + return () => { + navigationApi.unregisterTab('queue'); + }; + }, [rootApi, setIsLoading]); + + return ( + + + + ); +}); +QueueTabAutoLayout.displayName = 'QueueTabAutoLayout'; diff --git a/invokeai/frontend/web/src/features/ui/layouts/shared.ts b/invokeai/frontend/web/src/features/ui/layouts/shared.ts index 60dbb2e041..5dd836e37c 100644 --- a/invokeai/frontend/web/src/features/ui/layouts/shared.ts +++ b/invokeai/frontend/web/src/features/ui/layouts/shared.ts @@ -13,6 +13,9 @@ export const LAYERS_PANEL_ID = 'layers'; export const SETTINGS_PANEL_ID = 'settings'; +export const MODELS_PANEL_ID = 'models'; +export const QUEUE_PANEL_ID = 'queue'; + export const DEFAULT_TAB_ID = 'default-tab'; export const TAB_WITH_PROGRESS_INDICATOR_ID = 'tab-with-progress-indicator'; export const TAB_WITH_LAUNCHPAD_ICON_ID = 'tab-with-launchpad-icon';