mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 18:25:28 -05:00
refactor(ui): navigation api
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
export type Deferred<T> = {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (error: Error) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a promise and expose its resolve and reject callbacks.
|
||||
*/
|
||||
export const createDeferredPromise = <T>(): Deferred<T> => {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
@@ -15,7 +15,7 @@ export const CanvasLaunchpadPanel = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
const focusCanvas = useCallback(() => {
|
||||
navigationApi.focusPanelInTab(tab, WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel(tab, WORKSPACE_PANEL_ID);
|
||||
}, [tab]);
|
||||
return (
|
||||
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
|
||||
|
||||
@@ -24,7 +24,6 @@ export const BoardsPanel = memo(() => {
|
||||
const { tab } = useAutoLayoutContext();
|
||||
const collapsibleApi = useCollapsibleGridviewPanel(
|
||||
tab,
|
||||
'right',
|
||||
BOARDS_PANEL_ID,
|
||||
'vertical',
|
||||
BOARD_PANEL_DEFAULT_HEIGHT_PX,
|
||||
|
||||
@@ -35,7 +35,6 @@ export const GalleryPanel = memo(() => {
|
||||
const { tab } = useAutoLayoutContext();
|
||||
const collapsibleApi = useCollapsibleGridviewPanel(
|
||||
tab,
|
||||
'right',
|
||||
GALLERY_PANEL_ID,
|
||||
'vertical',
|
||||
GALLERY_PANEL_DEFAULT_HEIGHT_PX,
|
||||
|
||||
@@ -21,7 +21,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: false, type: 'raster_layer', dispatch, getState });
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -32,7 +32,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
const onClickNewCanvasWithControlLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: false, type: 'control_layer', dispatch, getState });
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -43,7 +43,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
const onClickNewCanvasWithRasterLayerFromImageWithResize = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: true, type: 'raster_layer', dispatch, getState });
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -54,7 +54,7 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
const onClickNewCanvasWithControlLayerFromImageWithResize = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await newCanvasFromImage({ imageDTO, withResize: true, type: 'control_layer', dispatch, getState });
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
|
||||
@@ -24,7 +24,7 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'raster_layer', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -36,7 +36,7 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'control_layer', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -48,7 +48,7 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'inpaint_mask', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -60,7 +60,7 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -72,7 +72,7 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const { dispatch, getState } = store;
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance_with_reference_image', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
|
||||
@@ -59,7 +59,7 @@ export const CurrentImageButtons = memo(() => {
|
||||
getState,
|
||||
dispatch,
|
||||
});
|
||||
navigationApi.focusPanelInTab('canvas', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
|
||||
// Automatically select the brush tool when editing an image
|
||||
if (canvasManager) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export const WorkflowViewEditToggleButton = memo(() => {
|
||||
dispatch(setActiveTab('workflows'));
|
||||
dispatch(workflowModeChanged('edit'));
|
||||
// Focus the Workflow Editor panel
|
||||
navigationApi.focusPanelInTab('workflows', WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
@@ -33,7 +33,7 @@ export const WorkflowViewEditToggleButton = memo(() => {
|
||||
dispatch(setActiveTab('workflows'));
|
||||
dispatch(workflowModeChanged('view'));
|
||||
// Focus the Image Viewer panel
|
||||
navigationApi.focusPanelInTab('workflows', VIEWER_PANEL_ID);
|
||||
navigationApi.focusPanel('workflows', VIEWER_PANEL_ID);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
@@ -63,18 +63,18 @@ export const useInvoke = () => {
|
||||
const enqueueBack = useCallback(() => {
|
||||
enqueue(false, false);
|
||||
if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') {
|
||||
navigationApi.focusPanelInTab(tabName, VIEWER_PANEL_ID);
|
||||
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
|
||||
} else if (tabName === 'canvas') {
|
||||
navigationApi.focusPanelInTab(tabName, WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel(tabName, WORKSPACE_PANEL_ID);
|
||||
}
|
||||
}, [enqueue, tabName]);
|
||||
|
||||
const enqueueFront = useCallback(() => {
|
||||
enqueue(true, false);
|
||||
if (tabName === 'generate' || tabName === 'workflows' || tabName === 'upscaling') {
|
||||
navigationApi.focusPanelInTab(tabName, VIEWER_PANEL_ID);
|
||||
navigationApi.focusPanel(tabName, VIEWER_PANEL_ID);
|
||||
} else if (tabName === 'canvas') {
|
||||
navigationApi.focusPanelInTab(tabName, WORKSPACE_PANEL_ID);
|
||||
navigationApi.focusPanel(tabName, WORKSPACE_PANEL_ID);
|
||||
}
|
||||
}, [enqueue, tabName]);
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/compone
|
||||
import { InvokeButtonTooltip } from 'features/queue/components/InvokeButtonTooltip/InvokeButtonTooltip';
|
||||
import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrentQueueItem';
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiCircleNotchBold,
|
||||
@@ -54,20 +53,12 @@ FloatingCanvasLeftPanelButtons.displayName = 'FloatingCanvasLeftPanelButtons';
|
||||
|
||||
const ToggleLeftPanelButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (navigationApi.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
navigationApi.toggleLeftPanelInTab(tab);
|
||||
}, [tab]);
|
||||
|
||||
return (
|
||||
<Tooltip label={t('accessibility.toggleLeftPanel')} placement="end">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleLeftPanel')}
|
||||
onClick={onClick}
|
||||
onClick={navigationApi.toggleLeftPanel}
|
||||
icon={<PiSlidersHorizontalBold />}
|
||||
flexGrow={1}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Flex, IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAutoLayoutContext } from 'features/ui/layouts/auto-layout-context';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesSquareBold } from 'react-icons/pi';
|
||||
|
||||
@@ -16,20 +15,12 @@ FloatingRightPanelButtons.displayName = 'FloatingRightPanelButtons';
|
||||
|
||||
const ToggleRightPanelButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (navigationApi.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
navigationApi.toggleRightPanelInTab(tab);
|
||||
}, [tab]);
|
||||
|
||||
return (
|
||||
<Tooltip label={t('accessibility.toggleRightPanel')} placement="start">
|
||||
<IconButton
|
||||
aria-label={t('accessibility.toggleRightPanel')}
|
||||
onClick={onClick}
|
||||
onClick={navigationApi.toggleRightPanel}
|
||||
icon={<PiImagesSquareBold />}
|
||||
h={48}
|
||||
/>
|
||||
|
||||
@@ -2,54 +2,26 @@ import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/us
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useAutoLayoutContext } from './auto-layout-context';
|
||||
|
||||
export const PanelHotkeysLogical = memo(() => {
|
||||
const { tab } = useAutoLayoutContext();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
category: 'app',
|
||||
id: 'toggleLeftPanel',
|
||||
callback: () => {
|
||||
if (navigationApi.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
navigationApi.toggleLeftPanelInTab(tab);
|
||||
},
|
||||
dependencies: [tab],
|
||||
callback: navigationApi.toggleLeftPanel,
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
category: 'app',
|
||||
id: 'toggleRightPanel',
|
||||
callback: () => {
|
||||
if (navigationApi.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
navigationApi.toggleRightPanelInTab(tab);
|
||||
},
|
||||
dependencies: [tab],
|
||||
callback: navigationApi.toggleRightPanel,
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
category: 'app',
|
||||
id: 'resetPanelLayout',
|
||||
callback: () => {
|
||||
if (navigationApi.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
navigationApi.resetPanelsInTab(tab);
|
||||
},
|
||||
dependencies: [tab],
|
||||
callback: navigationApi.resetLeftAndRightPanels,
|
||||
});
|
||||
useRegisteredHotkeys({
|
||||
category: 'app',
|
||||
id: 'togglePanels',
|
||||
callback: () => {
|
||||
if (navigationApi.tabApi?.getTab() !== tab) {
|
||||
return;
|
||||
}
|
||||
navigationApi.toggleBothPanelsInTab(tab);
|
||||
},
|
||||
dependencies: [tab],
|
||||
callback: navigationApi.toggleLeftAndRightPanels,
|
||||
});
|
||||
|
||||
return null;
|
||||
|
||||
@@ -113,6 +113,11 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, LAUNCHPAD_PANEL_ID, launchpad);
|
||||
navigationApi.registerPanel(tab, WORKSPACE_PANEL_ID, workspace);
|
||||
navigationApi.registerPanel(tab, VIEWER_PANEL_ID, viewer);
|
||||
|
||||
return { launchpad, workspace, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
@@ -122,7 +127,6 @@ const MainPanel = memo(() => {
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
const panels = initializeCenterPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'main', api);
|
||||
panels.launchpad.api.setActive();
|
||||
|
||||
const disposables = [
|
||||
@@ -211,6 +215,11 @@ export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: CANVAS_BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, GALLERY_PANEL_ID, gallery);
|
||||
navigationApi.registerPanel(tab, LAYERS_PANEL_ID, layers);
|
||||
navigationApi.registerPanel(tab, BOARDS_PANEL_ID, boards);
|
||||
|
||||
return { gallery, layers, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -220,7 +229,6 @@ const RightPanel = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeRightPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'right', api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -249,6 +257,9 @@ export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Register panel with navigation API
|
||||
navigationApi.registerPanel(tab, SETTINGS_PANEL_ID, settings);
|
||||
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -258,7 +269,6 @@ const LeftPanel = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeLeftPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'left', api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -309,6 +319,10 @@ export const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
navigationApi.registerPanel('canvas', LEFT_PANEL_ID, left);
|
||||
navigationApi.registerPanel('canvas', MAIN_PANEL_ID, main);
|
||||
navigationApi.registerPanel('canvas', RIGHT_PANEL_ID, right);
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -326,8 +340,9 @@ export const CanvasTabAutoLayout = memo(() => {
|
||||
return;
|
||||
}
|
||||
initializeRootPanelLayout(rootApi);
|
||||
navigationApi.registerPanel('canvas', 'root', rootApi);
|
||||
navigationApi.focusPanelInTab('canvas', LAUNCHPAD_PANEL_ID, false);
|
||||
|
||||
// Focus the launchpad panel once it's ready
|
||||
navigationApi.focusPanel('canvas', LAUNCHPAD_PANEL_ID);
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('canvas');
|
||||
|
||||
@@ -92,6 +92,10 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, LAUNCHPAD_PANEL_ID, launchpad);
|
||||
navigationApi.registerPanel(tab, VIEWER_PANEL_ID, viewer);
|
||||
|
||||
return { launchpad, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
@@ -101,7 +105,6 @@ const MainPanel = memo(() => {
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
const { launchpad } = initializeMainPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'main', api);
|
||||
launchpad.api.setActive();
|
||||
|
||||
const disposables = [
|
||||
@@ -175,6 +178,10 @@ export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, GALLERY_PANEL_ID, gallery);
|
||||
navigationApi.registerPanel(tab, BOARDS_PANEL_ID, boards);
|
||||
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -184,7 +191,6 @@ const RightPanel = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeRightPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'right', api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -213,6 +219,9 @@ export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Register panel with navigation API
|
||||
navigationApi.registerPanel(tab, SETTINGS_PANEL_ID, settings);
|
||||
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -222,7 +231,6 @@ const LeftPanel = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeLeftPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'left', api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -273,6 +281,10 @@ export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
navigationApi.registerPanel('generate', LEFT_PANEL_ID, left);
|
||||
navigationApi.registerPanel('generate', MAIN_PANEL_ID, main);
|
||||
navigationApi.registerPanel('generate', RIGHT_PANEL_ID, right);
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -290,8 +302,9 @@ export const GenerateTabAutoLayout = memo(() => {
|
||||
return;
|
||||
}
|
||||
initializeRootPanelLayout(rootApi);
|
||||
navigationApi.registerPanel('generate', 'root', rootApi);
|
||||
navigationApi.focusPanelInTab('generate', LAUNCHPAD_PANEL_ID, false);
|
||||
|
||||
// Focus the launchpad panel once it's ready
|
||||
navigationApi.focusPanel('generate', LAUNCHPAD_PANEL_ID);
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('generate');
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
import type { DockviewApi, GridviewApi, IDockviewPanel, IGridviewPanel } from 'dockview';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { GenerateTabLayout } from './navigation-api-2';
|
||||
import { AppNavigationApi } from './navigation-api-2';
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('app/logging/logger', () => ({
|
||||
logger: () => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock panel with setActive method
|
||||
const createMockPanel = () =>
|
||||
({
|
||||
api: {
|
||||
setActive: vi.fn(),
|
||||
},
|
||||
// Add other required properties as needed
|
||||
}) as unknown as IGridviewPanel;
|
||||
|
||||
const createMockDockPanel = () =>
|
||||
({
|
||||
api: {
|
||||
setActive: vi.fn(),
|
||||
},
|
||||
// Add other required properties as needed
|
||||
}) as unknown as IDockviewPanel;
|
||||
|
||||
// Create a mock layout for testing
|
||||
const createMockGenerateLayout = (): GenerateTabLayout => ({
|
||||
gridviewApi: {} as Readonly<GridviewApi>,
|
||||
panels: {
|
||||
left: {
|
||||
panelApi: {} as Readonly<IGridviewPanel>,
|
||||
gridviewApi: {} as Readonly<GridviewApi>,
|
||||
panels: {
|
||||
settings: createMockPanel(),
|
||||
},
|
||||
},
|
||||
main: {
|
||||
panelApi: {} as Readonly<IGridviewPanel>,
|
||||
dockviewApi: {} as Readonly<DockviewApi>,
|
||||
panels: {
|
||||
launchpad: createMockDockPanel(),
|
||||
viewer: createMockDockPanel(),
|
||||
},
|
||||
},
|
||||
right: {
|
||||
panelApi: {} as Readonly<IGridviewPanel>,
|
||||
gridviewApi: {} as Readonly<GridviewApi>,
|
||||
panels: {
|
||||
boards: createMockPanel(),
|
||||
gallery: createMockPanel(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('AppNavigationApi', () => {
|
||||
let navigationApi: AppNavigationApi;
|
||||
let mockSetAppTab: ReturnType<typeof vi.fn>;
|
||||
let mockGetAppTab: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
navigationApi = new AppNavigationApi();
|
||||
mockSetAppTab = vi.fn();
|
||||
mockGetAppTab = vi.fn();
|
||||
});
|
||||
|
||||
describe('Basic Connection', () => {
|
||||
it('should connect to app', () => {
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
|
||||
expect(navigationApi.setAppTab).toBe(mockSetAppTab);
|
||||
expect(navigationApi.getAppTab).toBe(mockGetAppTab);
|
||||
});
|
||||
|
||||
it('should disconnect from app', () => {
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
navigationApi.disconnectFromApp();
|
||||
|
||||
expect(navigationApi.setAppTab).toBeNull();
|
||||
expect(navigationApi.getAppTab).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tab Registration', () => {
|
||||
it('should register and unregister tabs', () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
const unregister = navigationApi.registerAppTab('generate', mockLayout);
|
||||
|
||||
expect(typeof unregister).toBe('function');
|
||||
|
||||
// Check that tab is registered using type assertion to access private property
|
||||
expect(navigationApi.appTabApi.generate).toBe(mockLayout);
|
||||
|
||||
// Unregister
|
||||
unregister();
|
||||
expect(navigationApi.appTabApi.generate).toBeNull();
|
||||
});
|
||||
|
||||
it('should notify waiters when tab is registered', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
|
||||
// Start waiting for registration
|
||||
const waitPromise = navigationApi.waitForTabRegistration('generate');
|
||||
|
||||
// Register the tab
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
|
||||
// Wait should resolve
|
||||
await expect(waitPromise).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel Focus', () => {
|
||||
beforeEach(() => {
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
});
|
||||
|
||||
it('should focus panel in already registered tab', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = await navigationApi.focusPanelInTab('generate', 'left', 'settings');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSetAppTab).not.toHaveBeenCalled();
|
||||
expect(mockLayout.panels.left.panels.settings.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should switch tab before focusing panel', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
mockGetAppTab.mockReturnValue('canvas'); // Currently on different tab
|
||||
|
||||
const result = await navigationApi.focusPanelInTab('generate', 'left', 'settings');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSetAppTab).toHaveBeenCalledWith('generate');
|
||||
expect(mockLayout.panels.left.panels.settings.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should wait for tab registration before focusing', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Start focus operation before tab is registered
|
||||
const focusPromise = navigationApi.focusPanelInTab('generate', 'left', 'settings');
|
||||
|
||||
// Register tab after a short delay
|
||||
setTimeout(() => {
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
}, 100);
|
||||
|
||||
const result = await focusPromise;
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockLayout.panels.left.panels.settings.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should focus different panel types', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Test gridview panel
|
||||
const result1 = await navigationApi.focusPanelInTab('generate', 'left', 'settings');
|
||||
expect(result1).toBe(true);
|
||||
expect(mockLayout.panels.left.panels.settings.api.setActive).toHaveBeenCalledOnce();
|
||||
|
||||
// Test dockview panel
|
||||
const result2 = await navigationApi.focusPanelInTab('generate', 'main', 'launchpad');
|
||||
expect(result2).toBe(true);
|
||||
expect(mockLayout.panels.main.panels.launchpad.api.setActive).toHaveBeenCalledOnce();
|
||||
|
||||
// Test right panel
|
||||
const result3 = await navigationApi.focusPanelInTab('generate', 'right', 'boards');
|
||||
expect(result3).toBe(true);
|
||||
expect(mockLayout.panels.right.panels.boards.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should return false on registration timeout', async () => {
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Set a short timeout for testing
|
||||
const result = await navigationApi.focusPanelInTab('generate', 'left', 'settings');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
|
||||
// Make setActive throw an error
|
||||
vi.mocked(mockLayout.panels.left.panels.settings.api.setActive).mockImplementation(() => {
|
||||
throw new Error('Mock error');
|
||||
});
|
||||
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = await navigationApi.focusPanelInTab('generate', 'left', 'settings');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should work without app connection', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
|
||||
// Don't connect to app
|
||||
const result = await navigationApi.focusPanelInTab('generate', 'left', 'settings');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockLayout.panels.left.panels.settings.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Registration Waiting', () => {
|
||||
it('should resolve immediately for already registered tabs', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
|
||||
const waitPromise = navigationApi.waitForTabRegistration('generate');
|
||||
|
||||
await expect(waitPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle multiple waiters', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
|
||||
const waitPromise1 = navigationApi.waitForTabRegistration('generate');
|
||||
const waitPromise2 = navigationApi.waitForTabRegistration('generate');
|
||||
|
||||
setTimeout(() => {
|
||||
navigationApi.registerAppTab('generate', mockLayout);
|
||||
}, 50);
|
||||
|
||||
await expect(Promise.all([waitPromise1, waitPromise2])).resolves.toEqual([undefined, undefined]);
|
||||
});
|
||||
|
||||
it('should timeout if tab is not registered', async () => {
|
||||
const waitPromise = navigationApi.waitForTabRegistration('generate', 100);
|
||||
|
||||
await expect(waitPromise).rejects.toThrow('Tab generate registration timed out');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
it('should handle complete workflow', async () => {
|
||||
const mockLayout = createMockGenerateLayout();
|
||||
|
||||
// Connect to app
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
mockGetAppTab.mockReturnValue('canvas');
|
||||
|
||||
// Register tab
|
||||
const unregister = navigationApi.registerAppTab('generate', mockLayout);
|
||||
|
||||
// Focus panel (should switch tab and focus)
|
||||
const result = await navigationApi.focusPanelInTab('generate', 'right', 'gallery');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSetAppTab).toHaveBeenCalledWith('generate');
|
||||
expect(mockLayout.panels.right.panels.gallery.api.setActive).toHaveBeenCalledOnce();
|
||||
|
||||
// Cleanup
|
||||
unregister();
|
||||
navigationApi.disconnectFromApp();
|
||||
|
||||
expect(navigationApi.setAppTab).toBeNull();
|
||||
expect(navigationApi.getAppTab).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle multiple tabs sequentially', async () => {
|
||||
const mockLayout1 = createMockGenerateLayout();
|
||||
const mockLayout2 = createMockGenerateLayout();
|
||||
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Register first tab
|
||||
const unregister1 = navigationApi.registerAppTab('generate', mockLayout1);
|
||||
|
||||
// Focus panel in first tab
|
||||
await navigationApi.focusPanelInTab('generate', 'left', 'settings');
|
||||
expect(mockLayout1.panels.left.panels.settings.api.setActive).toHaveBeenCalledOnce();
|
||||
|
||||
// Replace with second tab
|
||||
unregister1();
|
||||
navigationApi.registerAppTab('generate', mockLayout2);
|
||||
|
||||
// Focus panel in second tab
|
||||
await navigationApi.focusPanelInTab('generate', 'right', 'boards');
|
||||
expect(mockLayout2.panels.right.panels.boards.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { DockviewApi, GridviewApi, IDockviewPanel, IGridviewPanel } from 'dockview';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
export type GenerateTabLayout = {
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
left: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
settings: Readonly<IGridviewPanel>;
|
||||
};
|
||||
};
|
||||
main: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
dockviewApi: Readonly<DockviewApi>;
|
||||
panels: {
|
||||
launchpad: Readonly<IDockviewPanel>;
|
||||
viewer: Readonly<IDockviewPanel>;
|
||||
};
|
||||
};
|
||||
right: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
boards: Readonly<IGridviewPanel>;
|
||||
gallery: Readonly<IGridviewPanel>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type CanvasTabLayout = {
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
left: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
settings: Readonly<IGridviewPanel>;
|
||||
};
|
||||
};
|
||||
main: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
dockviewApi: Readonly<DockviewApi>;
|
||||
panels: {
|
||||
launchpad: Readonly<IDockviewPanel>;
|
||||
workspace: Readonly<IDockviewPanel>;
|
||||
viewer: Readonly<IDockviewPanel>;
|
||||
};
|
||||
};
|
||||
right: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
boards: Readonly<IGridviewPanel>;
|
||||
gallery: Readonly<IGridviewPanel>;
|
||||
layers: Readonly<IGridviewPanel>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type UpscalingTabLayout = {
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
left: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
settings: Readonly<IGridviewPanel>;
|
||||
};
|
||||
};
|
||||
main: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
dockviewApi: Readonly<DockviewApi>;
|
||||
panels: {
|
||||
launchpad: Readonly<IDockviewPanel>;
|
||||
viewer: Readonly<IDockviewPanel>;
|
||||
};
|
||||
};
|
||||
right: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
boards: Readonly<IGridviewPanel>;
|
||||
gallery: Readonly<IGridviewPanel>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowsTabLayout = {
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
left: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
settings: Readonly<IGridviewPanel>;
|
||||
};
|
||||
};
|
||||
main: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
dockviewApi: Readonly<DockviewApi>;
|
||||
panels: {
|
||||
launchpad: Readonly<IDockviewPanel>;
|
||||
workspace: Readonly<IDockviewPanel>;
|
||||
viewer: Readonly<IDockviewPanel>;
|
||||
};
|
||||
};
|
||||
right: {
|
||||
panelApi: Readonly<IGridviewPanel>;
|
||||
gridviewApi: Readonly<GridviewApi>;
|
||||
panels: {
|
||||
boards: Readonly<IGridviewPanel>;
|
||||
gallery: Readonly<IGridviewPanel>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type AppTabApi = {
|
||||
generate: GenerateTabLayout | null;
|
||||
canvas: CanvasTabLayout | null;
|
||||
upscaling: UpscalingTabLayout | null;
|
||||
workflows: WorkflowsTabLayout | null;
|
||||
};
|
||||
|
||||
export class AppNavigationApi {
|
||||
appTabApi: AppTabApi = {
|
||||
generate: null,
|
||||
canvas: null,
|
||||
upscaling: null,
|
||||
workflows: null,
|
||||
};
|
||||
|
||||
setAppTab: ((tab: TabName) => void) | null = null;
|
||||
getAppTab: (() => TabName) | null = null;
|
||||
|
||||
private registrationWaiters: Map<keyof AppTabApi, Array<() => void>> = new Map();
|
||||
|
||||
connectToApp(arg: { setAppTab: (tab: TabName) => void; getAppTab: () => TabName }): void {
|
||||
const { setAppTab, getAppTab } = arg;
|
||||
this.setAppTab = setAppTab;
|
||||
this.getAppTab = getAppTab;
|
||||
}
|
||||
|
||||
disconnectFromApp(): void {
|
||||
this.setAppTab = null;
|
||||
this.getAppTab = null;
|
||||
}
|
||||
|
||||
registerAppTab<T extends keyof AppTabApi>(tab: T, layout: AppTabApi[T]): () => void {
|
||||
this.appTabApi[tab] = layout;
|
||||
|
||||
// Notify any waiting consumers
|
||||
const waiters = this.registrationWaiters.get(tab);
|
||||
|
||||
if (waiters) {
|
||||
waiters.forEach((resolve) => resolve());
|
||||
this.registrationWaiters.delete(tab);
|
||||
}
|
||||
|
||||
return () => {
|
||||
this.appTabApi[tab] = null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus a specific panel in a specific tab
|
||||
* Automatically switches to the target tab if specified
|
||||
*/
|
||||
async focusPanelInTab<
|
||||
T extends keyof AppTabApi,
|
||||
R extends keyof NonNullable<AppTabApi[T]>['panels'],
|
||||
P extends keyof (NonNullable<AppTabApi[T]>['panels'][R] extends { panels: infer Panels } ? Panels : never),
|
||||
>(tabName: T, rootPanelId: R, panelId: P): Promise<boolean> {
|
||||
try {
|
||||
if (this.setAppTab && this.getAppTab && this.getAppTab() !== tabName) {
|
||||
this.setAppTab(tabName);
|
||||
}
|
||||
|
||||
await this.waitForTabRegistration(tabName);
|
||||
|
||||
const tabLayout = this.appTabApi[tabName];
|
||||
if (!tabLayout) {
|
||||
log.error(`Tab ${tabName} failed to register`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const panel = tabLayout.panels[rootPanelId].panels[panelId] as IGridviewPanel | IDockviewPanel;
|
||||
|
||||
panel.api.setActive();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to focus panel ${String(panelId)} in tab ${tabName}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
waitForTabRegistration<T extends keyof AppTabApi>(tabName: T, timeout = 1000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.appTabApi[tabName]) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let waiters = this.registrationWaiters.get(tabName);
|
||||
if (!waiters) {
|
||||
waiters = [];
|
||||
this.registrationWaiters.set(tabName, waiters);
|
||||
}
|
||||
waiters.push(resolve);
|
||||
const intervalId = setInterval(() => {
|
||||
if (this.appTabApi[tabName]) {
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
setTimeout(() => {
|
||||
clearInterval(intervalId);
|
||||
if (this.appTabApi[tabName]) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Tab ${tabName} registration timed out`));
|
||||
}
|
||||
}, timeout);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const navigationApi = new AppNavigationApi();
|
||||
@@ -0,0 +1,368 @@
|
||||
import type { IDockviewPanel, IGridviewPanel } from 'dockview';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { NavigationApi } from './navigation-api';
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('app/logging/logger', () => ({
|
||||
logger: () => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock panel with setActive method
|
||||
const createMockPanel = () =>
|
||||
({
|
||||
api: {
|
||||
setActive: vi.fn(),
|
||||
},
|
||||
}) as unknown as IGridviewPanel;
|
||||
|
||||
const createMockDockPanel = () =>
|
||||
({
|
||||
api: {
|
||||
setActive: vi.fn(),
|
||||
},
|
||||
}) as unknown as IDockviewPanel;
|
||||
|
||||
describe('AppNavigationApi', () => {
|
||||
let navigationApi: NavigationApi;
|
||||
let mockSetAppTab: ReturnType<typeof vi.fn>;
|
||||
let mockGetAppTab: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
navigationApi = new NavigationApi();
|
||||
mockSetAppTab = vi.fn();
|
||||
mockGetAppTab = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up all panels and pending promises to prevent unhandled rejections
|
||||
navigationApi.unregisterTab('generate');
|
||||
navigationApi.unregisterTab('canvas');
|
||||
navigationApi.unregisterTab('upscaling');
|
||||
navigationApi.unregisterTab('workflows');
|
||||
});
|
||||
|
||||
describe('Basic Connection', () => {
|
||||
it('should connect to app', () => {
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
|
||||
expect(navigationApi.setAppTab).toBe(mockSetAppTab);
|
||||
expect(navigationApi.getAppTab).toBe(mockGetAppTab);
|
||||
});
|
||||
|
||||
it('should disconnect from app', () => {
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
navigationApi.disconnectFromApp();
|
||||
|
||||
expect(navigationApi.setAppTab).toBeNull();
|
||||
expect(navigationApi.getAppTab).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel Registration', () => {
|
||||
it('should register and unregister panels', () => {
|
||||
const mockPanel = createMockPanel();
|
||||
const unregister = navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
|
||||
expect(typeof unregister).toBe('function');
|
||||
expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true);
|
||||
|
||||
// Unregister
|
||||
unregister();
|
||||
expect(navigationApi.isPanelRegistered('generate', 'settings')).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');
|
||||
|
||||
// Register the panel
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
|
||||
// Wait should resolve
|
||||
await expect(waitPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle multiple panels per tab', () => {
|
||||
const mockPanel1 = createMockPanel();
|
||||
const mockPanel2 = createMockDockPanel();
|
||||
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel1);
|
||||
navigationApi.registerPanel('generate', 'launchpad', mockPanel2);
|
||||
|
||||
expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true);
|
||||
expect(navigationApi.isPanelRegistered('generate', 'launchpad')).toBe(true);
|
||||
|
||||
const registeredPanels = navigationApi.getRegisteredPanels('generate');
|
||||
expect(registeredPanels).toContain('settings');
|
||||
expect(registeredPanels).toContain('launchpad');
|
||||
});
|
||||
|
||||
it('should handle panels across different tabs', () => {
|
||||
const mockPanel1 = createMockPanel();
|
||||
const mockPanel2 = createMockPanel();
|
||||
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel1);
|
||||
navigationApi.registerPanel('canvas', 'settings', mockPanel2);
|
||||
|
||||
expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true);
|
||||
expect(navigationApi.isPanelRegistered('canvas', 'settings')).toBe(true);
|
||||
|
||||
// Same panel ID in different tabs should be separate
|
||||
expect(navigationApi.getRegisteredPanels('generate')).toEqual(['settings']);
|
||||
expect(navigationApi.getRegisteredPanels('canvas')).toEqual(['settings']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel Focus', () => {
|
||||
beforeEach(() => {
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
});
|
||||
|
||||
it('should focus panel in already registered tab', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = await navigationApi.focusPanel('generate', 'settings');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSetAppTab).not.toHaveBeenCalled();
|
||||
expect(mockPanel.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should switch tab before focusing panel', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
mockGetAppTab.mockReturnValue('canvas'); // Currently on different tab
|
||||
|
||||
const result = await navigationApi.focusPanel('generate', 'settings');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSetAppTab).toHaveBeenCalledWith('generate');
|
||||
expect(mockPanel.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should wait for panel registration before focusing', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Start focus operation before panel is registered
|
||||
const focusPromise = navigationApi.focusPanel('generate', 'settings');
|
||||
|
||||
// Register panel after a short delay
|
||||
setTimeout(() => {
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
}, 100);
|
||||
|
||||
const result = await focusPromise;
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPanel.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should focus different panel types', async () => {
|
||||
const mockGridPanel = createMockPanel();
|
||||
const mockDockPanel = createMockDockPanel();
|
||||
|
||||
navigationApi.registerPanel('generate', 'settings', mockGridPanel);
|
||||
navigationApi.registerPanel('generate', 'launchpad', mockDockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Test gridview panel
|
||||
const result1 = await navigationApi.focusPanel('generate', 'settings');
|
||||
expect(result1).toBe(true);
|
||||
expect(mockGridPanel.api.setActive).toHaveBeenCalledOnce();
|
||||
|
||||
// Test dockview panel
|
||||
const result2 = await navigationApi.focusPanel('generate', 'launchpad');
|
||||
expect(result2).toBe(true);
|
||||
expect(mockDockPanel.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should return false on registration timeout', async () => {
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Set a short timeout for testing
|
||||
const result = await navigationApi.focusPanel('generate', 'settings');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
|
||||
// Make setActive throw an error
|
||||
vi.mocked(mockPanel.api.setActive).mockImplementation(() => {
|
||||
throw new Error('Mock error');
|
||||
});
|
||||
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
const result = await navigationApi.focusPanel('generate', 'settings');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should work without app connection', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
|
||||
// Don't connect to app
|
||||
const result = await navigationApi.focusPanel('generate', 'settings');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPanel.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Panel Waiting', () => {
|
||||
it('should resolve immediately for already registered panels', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
|
||||
const waitPromise = navigationApi.waitForPanel('generate', 'settings');
|
||||
|
||||
await expect(waitPromise).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle multiple waiters for same panel', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
|
||||
const waitPromise1 = navigationApi.waitForPanel('generate', 'settings');
|
||||
const waitPromise2 = navigationApi.waitForPanel('generate', 'settings');
|
||||
|
||||
setTimeout(() => {
|
||||
navigationApi.registerPanel('generate', 'settings', 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);
|
||||
|
||||
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);
|
||||
|
||||
await expect(waitPromise).rejects.toThrow('Panel generate:settings registration timed out after 200ms');
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
expect(elapsed).toBeGreaterThanOrEqual(200);
|
||||
expect(elapsed).toBeLessThan(300); // Allow some margin
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tab Management', () => {
|
||||
it('should unregister all panels for a tab', () => {
|
||||
const mockPanel1 = createMockPanel();
|
||||
const mockPanel2 = createMockPanel();
|
||||
const mockPanel3 = createMockPanel();
|
||||
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel1);
|
||||
navigationApi.registerPanel('generate', 'launchpad', mockPanel2);
|
||||
navigationApi.registerPanel('canvas', 'settings', mockPanel3);
|
||||
|
||||
expect(navigationApi.getRegisteredPanels('generate')).toHaveLength(2);
|
||||
expect(navigationApi.getRegisteredPanels('canvas')).toHaveLength(1);
|
||||
|
||||
navigationApi.unregisterTab('generate');
|
||||
|
||||
expect(navigationApi.getRegisteredPanels('generate')).toHaveLength(0);
|
||||
expect(navigationApi.getRegisteredPanels('canvas')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should clean up pending promises when unregistering tab', async () => {
|
||||
const waitPromise = navigationApi.waitForPanel('generate', 'settings', 5000);
|
||||
|
||||
navigationApi.unregisterTab('generate');
|
||||
|
||||
// The promise should reject with cancellation message since we cleaned up
|
||||
await expect(waitPromise).rejects.toThrow('Panel registration cancelled - tab generate was unregistered');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
it('should handle complete workflow', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
|
||||
// Connect to app
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
mockGetAppTab.mockReturnValue('canvas');
|
||||
|
||||
// Register panel
|
||||
const unregister = navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
|
||||
// Focus panel (should switch tab and focus)
|
||||
const result = await navigationApi.focusPanel('generate', 'settings');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSetAppTab).toHaveBeenCalledWith('generate');
|
||||
expect(mockPanel.api.setActive).toHaveBeenCalledOnce();
|
||||
|
||||
// Cleanup
|
||||
unregister();
|
||||
navigationApi.disconnectFromApp();
|
||||
|
||||
expect(navigationApi.setAppTab).toBeNull();
|
||||
expect(navigationApi.getAppTab).toBeNull();
|
||||
expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle multiple panels and tabs', async () => {
|
||||
const mockPanel1 = createMockPanel();
|
||||
const mockPanel2 = createMockDockPanel();
|
||||
const mockPanel3 = createMockPanel();
|
||||
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Register panels
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel1);
|
||||
navigationApi.registerPanel('generate', 'launchpad', mockPanel2);
|
||||
navigationApi.registerPanel('canvas', 'workspace', mockPanel3);
|
||||
|
||||
// Focus panels
|
||||
await navigationApi.focusPanel('generate', 'settings');
|
||||
expect(mockPanel1.api.setActive).toHaveBeenCalledOnce();
|
||||
|
||||
await navigationApi.focusPanel('generate', 'launchpad');
|
||||
expect(mockPanel2.api.setActive).toHaveBeenCalledOnce();
|
||||
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
await navigationApi.focusPanel('canvas', 'workspace');
|
||||
expect(mockSetAppTab).toHaveBeenCalledWith('canvas');
|
||||
expect(mockPanel3.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('should handle async registration and focus', async () => {
|
||||
const mockPanel = createMockPanel();
|
||||
mockGetAppTab.mockReturnValue('generate');
|
||||
|
||||
// Start focusing before registration
|
||||
const focusPromise = navigationApi.focusPanel('generate', 'settings');
|
||||
|
||||
// Register after delay
|
||||
setTimeout(() => {
|
||||
navigationApi.registerPanel('generate', 'settings', mockPanel);
|
||||
}, 50);
|
||||
|
||||
const result = await focusPromise;
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockPanel.api.setActive).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,212 +1,197 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { DockviewApi, GridviewApi, IGridviewPanel } from 'dockview';
|
||||
import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from 'features/ui/layouts/shared';
|
||||
import { createDeferredPromise, type Deferred } from 'common/util/createDeferredPromise';
|
||||
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';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
export type TabPanelApis = {
|
||||
root: Readonly<GridviewApi> | null;
|
||||
left: Readonly<GridviewApi> | null;
|
||||
main: Readonly<DockviewApi> | null;
|
||||
right: Readonly<GridviewApi> | null;
|
||||
type PanelType = IGridviewPanel | IDockviewPanel;
|
||||
|
||||
type Waiter = {
|
||||
deferred: Deferred<void>;
|
||||
timeoutId: ReturnType<typeof setTimeout> | null;
|
||||
};
|
||||
|
||||
const getInitialTabPanelApis = (): TabPanelApis => ({
|
||||
root: null,
|
||||
left: null,
|
||||
main: null,
|
||||
right: null,
|
||||
});
|
||||
const PANEL_ENABLED_TABS: TabName[] = ['canvas', 'generate', 'workflows', 'queue'];
|
||||
|
||||
type AppTabApi = {
|
||||
setTab: (tabName: TabName) => void;
|
||||
getTab: () => TabName;
|
||||
};
|
||||
export class NavigationApi {
|
||||
private panels: Map<string, PanelType> = new Map();
|
||||
private waiters: Map<string, Waiter> = new Map();
|
||||
|
||||
/**
|
||||
* Global API for managing application navigation and tab panels.
|
||||
*/
|
||||
export class AppNavigationApi {
|
||||
private tabPanelApis = new Map<TabName, TabPanelApis>();
|
||||
setAppTab: ((tab: TabName) => void) | null = null;
|
||||
getAppTab: (() => TabName) | null = null;
|
||||
|
||||
tabApi: AppTabApi | null = null;
|
||||
connectToApp = (arg: { setAppTab: (tab: TabName) => void; getAppTab: () => TabName }): void => {
|
||||
const { setAppTab, getAppTab } = arg;
|
||||
this.setAppTab = setAppTab;
|
||||
this.getAppTab = getAppTab;
|
||||
};
|
||||
|
||||
disconnectFromApp = (): void => {
|
||||
this.setAppTab = null;
|
||||
this.getAppTab = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the Redux store reference for tab switching
|
||||
* Register a panel with a unique ID
|
||||
* @param tab - The tab this panel belongs to
|
||||
* @param panelId - Unique identifier for the panel
|
||||
* @param panel - The panel instance
|
||||
* @returns Cleanup function to unregister the panel
|
||||
*/
|
||||
setTabApi(tabApi: AppTabApi): void {
|
||||
this.tabApi = tabApi;
|
||||
}
|
||||
registerPanel = (tab: TabName, panelId: string, panel: PanelType): (() => void) => {
|
||||
const key = `${tab}:${panelId}`;
|
||||
|
||||
/**
|
||||
* Register panel APIs for a specific tab
|
||||
*/
|
||||
registerPanel<T extends keyof TabPanelApis>(tab: TabName, panel: T, api: NonNullable<TabPanelApis[T]>): void {
|
||||
const current = this.tabPanelApis.get(tab) ?? getInitialTabPanelApis();
|
||||
const apis: TabPanelApis = {
|
||||
...current,
|
||||
[panel]: api,
|
||||
this.panels.set(key, panel);
|
||||
|
||||
// Resolve any waiting promises
|
||||
const waiter = this.waiters.get(key);
|
||||
if (waiter) {
|
||||
if (waiter.timeoutId) {
|
||||
// Clear the timeout if it exists
|
||||
clearTimeout(waiter.timeoutId);
|
||||
}
|
||||
waiter.deferred.resolve();
|
||||
this.waiters.delete(key);
|
||||
}
|
||||
|
||||
log.debug(`Registered panel ${key}`);
|
||||
|
||||
return () => {
|
||||
this.panels.delete(key);
|
||||
log.debug(`Unregistered panel ${key}`);
|
||||
};
|
||||
this.tabPanelApis.set(tab, apis);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unregister panel APIs for a specific tab
|
||||
* Wait for a panel to be ready
|
||||
* @param tab - The tab the panel belongs to
|
||||
* @param panelId - The panel ID to wait for
|
||||
* @param timeout - Timeout in milliseconds (default: 2000)
|
||||
* @returns Promise that resolves when the panel is ready
|
||||
*/
|
||||
unregisterPanel<T extends keyof TabPanelApis>(tab: TabName, panel: T): void {
|
||||
const current = this.tabPanelApis.get(tab);
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
const apis: TabPanelApis = {
|
||||
...current,
|
||||
[panel]: null,
|
||||
};
|
||||
this.tabPanelApis.set(tab, apis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister panel APIs for a specific tab
|
||||
*/
|
||||
unregisterTab(tabName: TabName): void {
|
||||
this.tabPanelApis.delete(tabName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get panel APIs for a specific tab
|
||||
*/
|
||||
getTabPanelApis(tabName: TabName): TabPanelApis | null {
|
||||
return this.tabPanelApis.get(tabName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get panel APIs for the currently active tab
|
||||
*/
|
||||
getActiveTabPanelApis(): TabPanelApis | null {
|
||||
if (!this.tabApi) {
|
||||
return null;
|
||||
waitForPanel = (tab: TabName, panelId: string, timeout = 2000): Promise<void> => {
|
||||
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 activeTab = this.tabApi.getTab();
|
||||
return this.getTabPanelApis(activeTab);
|
||||
}
|
||||
const key = `${tab}:${panelId}`;
|
||||
|
||||
/**
|
||||
* Get all registered tab names
|
||||
*/
|
||||
getRegisteredTabs(): TabName[] {
|
||||
return Array.from(this.tabPanelApis.keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tab is registered
|
||||
*/
|
||||
isTabRegistered(tabName: TabName): boolean {
|
||||
return this.tabPanelApis.has(tabName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a specific tab
|
||||
*/
|
||||
private switchToTab(tabName: TabName): boolean {
|
||||
if (!this.tabApi) {
|
||||
log.warn(`Cannot switch to tab "${tabName}": no store reference`);
|
||||
return false;
|
||||
if (this.panels.has(key)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.tabApi.setTab(tabName);
|
||||
return true;
|
||||
}
|
||||
// Check if we already have a promise for this panel
|
||||
const existing = this.waiters.get(key);
|
||||
|
||||
if (existing) {
|
||||
return existing.deferred.promise;
|
||||
}
|
||||
|
||||
const deferred = createDeferredPromise<void>();
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Only reject if this deferred is still waiting
|
||||
const waiter = this.waiters.get(key);
|
||||
if (waiter) {
|
||||
this.waiters.delete(key);
|
||||
deferred.reject(new Error(`Panel ${key} registration timed out after ${timeout}ms`));
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
this.waiters.set(key, { deferred, timeoutId });
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
getPanelKey = (tab: TabName, panelId: string): string => {
|
||||
return `${tab}:${panelId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Focus a specific panel in a specific tab
|
||||
* Automatically switches to the target tab if specified
|
||||
* @param tab - The tab to switch to
|
||||
* @param panelId - The panel ID to focus
|
||||
* @returns Promise that resolves to true if successful, false otherwise
|
||||
*/
|
||||
focusPanelInTab(tabName: TabName, panelId: string, switchTab = true): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis) {
|
||||
log.warn(`Tab "${tabName}" not registered`);
|
||||
return false;
|
||||
focusPanel = async (tab: TabName, panelId: string): Promise<boolean> => {
|
||||
if (!PANEL_ENABLED_TABS.includes(tab)) {
|
||||
log.error(`Tab ${tab} is not enabled for panel registration`);
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
if (switchTab) {
|
||||
// Switch to target tab first
|
||||
if (!this.switchToTab(tabName)) {
|
||||
try {
|
||||
// Switch to the target tab if needed
|
||||
if (this.setAppTab && this.getAppTab && this.getAppTab() !== tab) {
|
||||
this.setAppTab(tab);
|
||||
}
|
||||
|
||||
// Wait for the panel to be ready
|
||||
await this.waitForPanel(tab, panelId);
|
||||
|
||||
const key = this.getPanelKey(tab, panelId);
|
||||
const panel = this.panels.get(key);
|
||||
|
||||
if (!panel) {
|
||||
log.error(`Panel ${key} not found after waiting`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to focus in main panel (dockview) first
|
||||
if (apis.main) {
|
||||
const panel = apis.main.getPanel(panelId);
|
||||
if (panel) {
|
||||
panel.api.setActive();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Focus the panel
|
||||
panel.api.setActive();
|
||||
log.debug(`Focused panel ${key}`);
|
||||
|
||||
// Try left panel
|
||||
if (apis.left) {
|
||||
const panel = apis.left.getPanel(panelId);
|
||||
if (panel) {
|
||||
panel.api.setActive();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Try right panel
|
||||
if (apis.right) {
|
||||
const panel = apis.right.getPanel(panelId);
|
||||
if (panel) {
|
||||
panel.api.setActive();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
log.warn(`Panel "${panelId}" not found in tab "${tabName}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus a panel in the currently active tab
|
||||
*/
|
||||
focusPanelInActiveTab(panelId: string): boolean {
|
||||
if (!this.tabApi) {
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(`Failed to focus panel ${panelId} in tab ${tab}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.focusPanelInTab(activeTab, panelId);
|
||||
}
|
||||
focusPanelInActiveTab = (panelId: string): Promise<boolean> => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.error('No active tab found');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return this.focusPanel(activeTab, panelId);
|
||||
};
|
||||
|
||||
expandPanel(panel: IGridviewPanel, width: number) {
|
||||
expandPanel = (panel: IGridviewPanel, width: number) => {
|
||||
panel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: width });
|
||||
panel.api.setSize({ width: width });
|
||||
}
|
||||
};
|
||||
|
||||
collapsePanel(panel: IGridviewPanel) {
|
||||
collapsePanel = (panel: IGridviewPanel) => {
|
||||
panel.api.setConstraints({ maximumWidth: 0, minimumWidth: 0 });
|
||||
panel.api.setSize({ width: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the left panel in a specific tab
|
||||
*/
|
||||
toggleLeftPanelInTab(tabName: TabName): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis?.root) {
|
||||
log.warn(`Root panel API not available for tab "${tabName}"`);
|
||||
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);
|
||||
};
|
||||
|
||||
toggleLeftPanel = (): boolean => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.warn('No active tab found to toggle left panel');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switchToTab(tabName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const leftPanel = apis.root.getPanel('left');
|
||||
const leftPanel = this.getPanel(activeTab, 'left');
|
||||
if (!leftPanel) {
|
||||
log.warn(`Left panel not found in tab "${tabName}"`);
|
||||
log.warn(`Left panel not found in active tab "${activeTab}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(leftPanel instanceof GridviewPanel)) {
|
||||
log.error(`Right panels must be instances of GridviewPanel`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -217,25 +202,22 @@ export class AppNavigationApi {
|
||||
this.collapsePanel(leftPanel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle the right panel in a specific tab
|
||||
*/
|
||||
toggleRightPanelInTab(tabName: TabName): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis?.root) {
|
||||
log.warn(`Root panel API not available for tab "${tabName}"`);
|
||||
toggleRightPanel = (): boolean => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.warn('No active tab found to toggle right panel');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switchToTab(tabName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rightPanel = apis.root.getPanel('right');
|
||||
const rightPanel = this.getPanel(activeTab, 'right');
|
||||
if (!rightPanel) {
|
||||
log.warn(`Right panel not found in tab "${tabName}"`);
|
||||
log.warn(`Right panel not found in active tab "${activeTab}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(rightPanel instanceof GridviewPanel)) {
|
||||
log.error(`Right panels must be instances of GridviewPanel`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -246,19 +228,24 @@ export class AppNavigationApi {
|
||||
this.collapsePanel(rightPanel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
toggleBothPanelsInTab(tabName: TabName): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis?.root) {
|
||||
log.warn(`Root panel API not available for tab "${tabName}"`);
|
||||
toggleLeftAndRightPanels = (): boolean => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.warn('No active tab found to toggle right panel');
|
||||
return false;
|
||||
}
|
||||
const leftPanel = this.getPanel(activeTab, 'left');
|
||||
const rightPanel = this.getPanel(activeTab, 'right');
|
||||
|
||||
if (!rightPanel || !leftPanel) {
|
||||
log.warn(`Right and/or left panel not found in tab "${activeTab}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const rightPanel = apis.root.getPanel('right');
|
||||
const leftPanel = apis.root.getPanel('left');
|
||||
if (!rightPanel || !leftPanel) {
|
||||
log.warn(`Right and/or left panel not found in tab "${tabName}"`);
|
||||
if (!(leftPanel instanceof GridviewPanel) || !(rightPanel instanceof GridviewPanel)) {
|
||||
log.error(`Left and right panels must be instances of GridviewPanel`);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -273,75 +260,90 @@ export class AppNavigationApi {
|
||||
this.collapsePanel(rightPanel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the left panel in the currently active tab
|
||||
*/
|
||||
toggleLeftPanelInActiveTab(): boolean {
|
||||
if (!this.tabApi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.toggleLeftPanelInTab(activeTab);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the right panel in the currently active tab
|
||||
*/
|
||||
toggleRightPanelInActiveTab(): boolean {
|
||||
if (!this.tabApi) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.toggleRightPanelInTab(activeTab);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset panels in a specific tab (expand both left and right)
|
||||
*/
|
||||
resetPanelsInTab(tabName: TabName): boolean {
|
||||
const apis = this.getTabPanelApis(tabName);
|
||||
if (!apis?.root) {
|
||||
log.warn(`Root panel API not available for tab "${tabName}"`);
|
||||
resetLeftAndRightPanels = (): boolean => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.warn('No active tab found to toggle right panel');
|
||||
return false;
|
||||
}
|
||||
const leftPanel = this.getPanel(activeTab, 'left');
|
||||
const rightPanel = this.getPanel(activeTab, 'right');
|
||||
|
||||
if (!rightPanel || !leftPanel) {
|
||||
log.warn(`Right and/or left panel not found in tab "${activeTab}"`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.switchToTab(tabName)) {
|
||||
if (!(leftPanel instanceof GridviewPanel) || !(rightPanel instanceof GridviewPanel)) {
|
||||
log.error(`Left and right panels must be instances of GridviewPanel`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const rootApi = apis.root as GridviewApi;
|
||||
const leftPanel = rootApi.getPanel('left');
|
||||
const rightPanel = rootApi.getPanel('right');
|
||||
leftPanel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: LEFT_PANEL_MIN_SIZE_PX });
|
||||
leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
if (leftPanel) {
|
||||
leftPanel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: LEFT_PANEL_MIN_SIZE_PX });
|
||||
leftPanel.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
}
|
||||
|
||||
if (rightPanel) {
|
||||
rightPanel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
}
|
||||
rightPanel.api.setConstraints({ maximumWidth: Number.MAX_SAFE_INTEGER, minimumWidth: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
rightPanel.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset panels in the currently active tab
|
||||
* Check if a panel is registered
|
||||
* @param tab - The tab the panel belongs to
|
||||
* @param panelId - The panel ID to check
|
||||
* @returns True if the panel is registered
|
||||
*/
|
||||
resetPanelsInActiveTab(): boolean {
|
||||
if (!this.tabApi) {
|
||||
return false;
|
||||
isPanelRegistered = (tab: TabName, panelId: string): boolean => {
|
||||
const key = `${tab}:${panelId}`;
|
||||
return this.panels.has(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all registered panels for a tab
|
||||
* @param tab - The tab to get panels for
|
||||
* @returns Array of panel IDs
|
||||
*/
|
||||
getRegisteredPanels = (tab: TabName): string[] => {
|
||||
const prefix = `${tab}:`;
|
||||
return Array.from(this.panels.keys())
|
||||
.filter((key) => key.startsWith(prefix))
|
||||
.map((key) => key.substring(prefix.length));
|
||||
};
|
||||
|
||||
/**
|
||||
* Unregister all panels for a tab
|
||||
* @param tab - The tab to unregister panels for
|
||||
*/
|
||||
unregisterTab = (tab: TabName): void => {
|
||||
const prefix = `${tab}:`;
|
||||
const keysToDelete = Array.from(this.panels.keys()).filter((key) => key.startsWith(prefix));
|
||||
|
||||
for (const key of keysToDelete) {
|
||||
this.panels.delete(key);
|
||||
}
|
||||
|
||||
const activeTab = this.tabApi.getTab();
|
||||
return this.resetPanelsInTab(activeTab);
|
||||
}
|
||||
// Clean up any pending promises by rejecting them
|
||||
const promiseKeysToDelete = Array.from(this.waiters.keys()).filter((key) => key.startsWith(prefix));
|
||||
for (const key of promiseKeysToDelete) {
|
||||
const waiter = this.waiters.get(key);
|
||||
if (waiter) {
|
||||
// Clear timeout before rejecting
|
||||
if (waiter.timeoutId) {
|
||||
clearTimeout(waiter.timeoutId);
|
||||
}
|
||||
waiter.deferred.reject(new Error(`Panel registration cancelled - tab ${tab} was unregistered`));
|
||||
}
|
||||
this.waiters.delete(key);
|
||||
}
|
||||
|
||||
log.debug(`Unregistered all panels for tab ${tab}`);
|
||||
};
|
||||
}
|
||||
|
||||
// Global singleton instance
|
||||
export const navigationApi = new AppNavigationApi();
|
||||
export const navigationApi = new NavigationApi();
|
||||
|
||||
@@ -92,6 +92,10 @@ const initializeCenterPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, LAUNCHPAD_PANEL_ID, launchpad);
|
||||
navigationApi.registerPanel(tab, VIEWER_PANEL_ID, viewer);
|
||||
|
||||
return { launchpad, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
@@ -101,7 +105,6 @@ const MainPanel = memo(() => {
|
||||
({ api }) => {
|
||||
const panels = initializeCenterPanelLayout(tab, api);
|
||||
panels.launchpad.api.setActive();
|
||||
navigationApi.registerPanel(tab, 'main', api);
|
||||
|
||||
const disposables = [
|
||||
api.onWillShowOverlay((e) => {
|
||||
@@ -174,6 +177,10 @@ export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, GALLERY_PANEL_ID, gallery);
|
||||
navigationApi.registerPanel(tab, BOARDS_PANEL_ID, boards);
|
||||
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -183,7 +190,6 @@ const RightPanel = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeRightPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'right', api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -212,6 +218,9 @@ export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Register panel with navigation API
|
||||
navigationApi.registerPanel(tab, SETTINGS_PANEL_ID, settings);
|
||||
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -221,7 +230,6 @@ const LeftPanel = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeLeftPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'left', api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -273,6 +281,10 @@ export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
navigationApi.registerPanel('upscaling', LEFT_PANEL_ID, left);
|
||||
navigationApi.registerPanel('upscaling', MAIN_PANEL_ID, main);
|
||||
navigationApi.registerPanel('upscaling', RIGHT_PANEL_ID, right);
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -290,8 +302,9 @@ export const UpscalingTabAutoLayout = memo(() => {
|
||||
return;
|
||||
}
|
||||
initializeRootPanelLayout(rootApi);
|
||||
navigationApi.registerPanel('upscaling', 'root', rootApi);
|
||||
navigationApi.focusPanelInTab('upscaling', LAUNCHPAD_PANEL_ID, false);
|
||||
|
||||
// Focus the launchpad panel once it's ready
|
||||
navigationApi.focusPanel('upscaling', LAUNCHPAD_PANEL_ID);
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('upscaling');
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { GridviewPanelApi, IGridviewPanel } from 'dockview';
|
||||
import { GridviewPanel, type GridviewPanelApi, type IGridviewPanel } from 'dockview';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { TabPanelApis } from './navigation-api';
|
||||
import { navigationApi } from './navigation-api';
|
||||
|
||||
const getIsCollapsed = (
|
||||
@@ -19,7 +18,6 @@ const getIsCollapsed = (
|
||||
|
||||
export const useCollapsibleGridviewPanel = (
|
||||
tab: TabName,
|
||||
rootPanelId: Exclude<keyof TabPanelApis, 'main'>,
|
||||
panelId: string,
|
||||
orientation: 'horizontal' | 'vertical',
|
||||
defaultSize: number,
|
||||
@@ -28,12 +26,9 @@ export const useCollapsibleGridviewPanel = (
|
||||
const $isCollapsed = useState(() => atom(false))[0];
|
||||
const lastExpandedSizeRef = useRef<number>(0);
|
||||
const collapse = useCallback(() => {
|
||||
const api = navigationApi.getTabPanelApis(tab)?.[rootPanelId];
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
const panel = api.getPanel(panelId);
|
||||
if (!panel) {
|
||||
const panel = navigationApi.getPanel(tab, panelId);
|
||||
|
||||
if (!panel || !(panel instanceof GridviewPanel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,16 +39,11 @@ export const useCollapsibleGridviewPanel = (
|
||||
} else {
|
||||
panel.api.setSize({ width: collapsedSize ?? panel.minimumWidth });
|
||||
}
|
||||
}, [collapsedSize, orientation, panelId, rootPanelId, tab]);
|
||||
}, [collapsedSize, orientation, panelId, tab]);
|
||||
|
||||
const expand = useCallback(() => {
|
||||
const api = navigationApi.getTabPanelApis(tab)?.[rootPanelId];
|
||||
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
const panel = api.getPanel(panelId);
|
||||
if (!panel) {
|
||||
const panel = navigationApi.getPanel(tab, panelId);
|
||||
if (!panel || !(panel instanceof GridviewPanel)) {
|
||||
return;
|
||||
}
|
||||
if (orientation === 'vertical') {
|
||||
@@ -61,16 +51,11 @@ export const useCollapsibleGridviewPanel = (
|
||||
} else {
|
||||
panel.api.setSize({ width: lastExpandedSizeRef.current || defaultSize });
|
||||
}
|
||||
}, [defaultSize, orientation, panelId, rootPanelId, tab]);
|
||||
}, [defaultSize, orientation, panelId, tab]);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
const api = navigationApi.getTabPanelApis(tab)?.[rootPanelId];
|
||||
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
const panel = api.getPanel(panelId);
|
||||
if (!panel) {
|
||||
const panel = navigationApi.getPanel(tab, panelId);
|
||||
if (!panel || !(panel instanceof GridviewPanel)) {
|
||||
return;
|
||||
}
|
||||
const isCollapsed = getIsCollapsed(panel, orientation, collapsedSize);
|
||||
@@ -79,16 +64,11 @@ export const useCollapsibleGridviewPanel = (
|
||||
} else {
|
||||
collapse();
|
||||
}
|
||||
}, [tab, rootPanelId, panelId, orientation, collapsedSize, expand, collapse]);
|
||||
}, [tab, panelId, orientation, collapsedSize, expand, collapse]);
|
||||
|
||||
useEffect(() => {
|
||||
const api = navigationApi.getTabPanelApis(tab)?.[rootPanelId];
|
||||
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
const panel = api.getPanel(panelId);
|
||||
if (!panel) {
|
||||
const panel = navigationApi.getPanel(tab, panelId);
|
||||
if (!panel || !(panel instanceof GridviewPanel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,7 +80,7 @@ export const useCollapsibleGridviewPanel = (
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [$isCollapsed, collapsedSize, orientation, panelId, rootPanelId, tab]);
|
||||
}, [$isCollapsed, collapsedSize, orientation, panelId, tab]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -3,10 +3,9 @@ import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { navigationApi } from './navigation-api';
|
||||
import { navigationApi as navigationApi2 } from './navigation-api-2';
|
||||
|
||||
/**
|
||||
* Hook that initializes the global navigation API with callbacks to access and modify the active tab.
|
||||
@@ -14,21 +13,6 @@ import { navigationApi as navigationApi2 } from './navigation-api-2';
|
||||
export const useNavigationApi = () => {
|
||||
useAssertSingleton('useNavigationApi');
|
||||
const store = useAppStore();
|
||||
const tabApi = useMemo(
|
||||
() => ({
|
||||
getTab: () => {
|
||||
return selectActiveTab(store.getState());
|
||||
},
|
||||
setTab: (tab: TabName) => {
|
||||
store.dispatch(setActiveTab(tab));
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
navigationApi.setTabApi(tabApi);
|
||||
}, [store, tabApi]);
|
||||
|
||||
const getAppTab = useCallback(() => {
|
||||
return selectActiveTab(store.getState());
|
||||
@@ -40,6 +24,6 @@ export const useNavigationApi = () => {
|
||||
[store]
|
||||
);
|
||||
useEffect(() => {
|
||||
navigationApi2.connectToApp({ getAppTab, setAppTab });
|
||||
}, [getAppTab, setAppTab, store, tabApi]);
|
||||
navigationApi.connectToApp({ getAppTab, setAppTab });
|
||||
}, [getAppTab, setAppTab, store]);
|
||||
};
|
||||
|
||||
@@ -110,6 +110,11 @@ const initializeMainPanelLayout = (tab: TabName, api: DockviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, LAUNCHPAD_PANEL_ID, launchpad);
|
||||
navigationApi.registerPanel(tab, WORKSPACE_PANEL_ID, workspace);
|
||||
navigationApi.registerPanel(tab, VIEWER_PANEL_ID, viewer);
|
||||
|
||||
return { launchpad, workspace, viewer } satisfies Record<string, IDockviewPanel>;
|
||||
};
|
||||
|
||||
@@ -119,7 +124,6 @@ const MainPanel = memo(() => {
|
||||
const onReady = useCallback<IDockviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
const panels = initializeMainPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'main', api);
|
||||
panels.launchpad.api.setActive();
|
||||
|
||||
const disposables = [
|
||||
@@ -193,6 +197,10 @@ export const initializeRightPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
gallery.api.setSize({ height: GALLERY_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
boards.api.setSize({ height: BOARD_PANEL_DEFAULT_HEIGHT_PX, width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
// Register panels with navigation API
|
||||
navigationApi.registerPanel(tab, GALLERY_PANEL_ID, gallery);
|
||||
navigationApi.registerPanel(tab, BOARDS_PANEL_ID, boards);
|
||||
|
||||
return { gallery, boards } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -202,7 +210,6 @@ const RightPanel = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeRightPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'right', api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -231,6 +238,9 @@ export const initializeLeftPanelLayout = (tab: TabName, api: GridviewApi) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Register panel with navigation API
|
||||
navigationApi.registerPanel(tab, SETTINGS_PANEL_ID, settings);
|
||||
|
||||
return { settings } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -240,7 +250,6 @@ const LeftPanel = memo(() => {
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(
|
||||
({ api }) => {
|
||||
initializeLeftPanelLayout(tab, api);
|
||||
navigationApi.registerPanel(tab, 'left', api);
|
||||
},
|
||||
[tab]
|
||||
);
|
||||
@@ -285,9 +294,14 @@ export const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
referencePanel: MAIN_PANEL_ID,
|
||||
},
|
||||
});
|
||||
|
||||
left.api.setSize({ width: LEFT_PANEL_MIN_SIZE_PX });
|
||||
right.api.setSize({ width: RIGHT_PANEL_MIN_SIZE_PX });
|
||||
|
||||
navigationApi.registerPanel('workflows', LEFT_PANEL_ID, left);
|
||||
navigationApi.registerPanel('workflows', MAIN_PANEL_ID, main);
|
||||
navigationApi.registerPanel('workflows', RIGHT_PANEL_ID, right);
|
||||
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
@@ -305,8 +319,9 @@ export const WorkflowsTabAutoLayout = memo(() => {
|
||||
return;
|
||||
}
|
||||
initializeRootPanelLayout(rootApi);
|
||||
navigationApi.registerPanel('workflows', 'root', rootApi);
|
||||
navigationApi.focusPanelInTab('workflows', LAUNCHPAD_PANEL_ID, false);
|
||||
|
||||
// Focus the launchpad panel once it's ready
|
||||
navigationApi.focusPanel('workflows', LAUNCHPAD_PANEL_ID);
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('workflows');
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppDispatch } from 'app/store/store';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import type { Deferred } from 'common/util/createDeferredPromise';
|
||||
import { createDeferredPromise } from 'common/util/createDeferredPromise';
|
||||
import { withResultAsync, WrappedError } from 'common/util/result';
|
||||
import { parseify } from 'common/util/serialize';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
@@ -15,27 +17,6 @@ import type { EnqueueBatchArg } from './types';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
type Deferred<T> = {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (error: Error) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a promise and expose its resolve and reject callbacks.
|
||||
*/
|
||||
const createDeferredPromise = <T>(): Deferred<T> => {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
type QueueStatusEventHandler = {
|
||||
subscribe: (handler: (event: S['QueueItemStatusChangedEvent']) => void) => void;
|
||||
unsubscribe: (handler: (event: S['QueueItemStatusChangedEvent']) => void) => void;
|
||||
|
||||
Reference in New Issue
Block a user