feat(canvas): implement Phase 7.2 - functional canvas instance manager

- Extended NavigationApi with getDockviewApi() and getContainer() methods to allow component access to dockview APIs
- Enhanced CanvasInstanceManager to create dockview panels dynamically when "Add Canvas" is clicked
- Added selectCanvasInstances selector for future use
- CanvasInstanceManager now creates both Redux state and corresponding dockview panel
- New panels are automatically activated when created
- Maximum 3 canvas limit enforced

This completes the core "Add new canvas" functionality specified in Phase 7.2.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
psychedelicious
2025-09-02 18:31:44 +10:00
parent 21b1555c64
commit de255819b7
3 changed files with 79 additions and 10 deletions

View File

@@ -46,6 +46,11 @@ export const selectActiveCanvasId = (state: RootState) => state.canvases.activeI
*/
export const selectCanvasCount = (state: RootState) => Object.keys(state.canvases.instances).length;
/**
* Selects all canvas instances
*/
export const selectCanvasInstances = (state: RootState) => state.canvases.instances;
/**
* Legacy selector for backward compatibility - selects the active canvas
* @deprecated Use selectActiveCanvas instead

View File

@@ -2,6 +2,12 @@ import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { canvasInstanceAdded } from 'features/controlLayers/store/canvasesSlice';
import { selectCanvasCount } from 'features/controlLayers/store/selectors';
import type { DockviewPanelParameters } from 'features/ui/layouts/auto-layout-context';
import { navigationApi } from 'features/ui/layouts/navigation-api';
import {
DOCKVIEW_TAB_CANVAS_WORKSPACE_ID,
WORKSPACE_PANEL_ID
} from 'features/ui/layouts/shared';
import { nanoid } from 'nanoid';
import { memo, useCallback } from 'react';
import { PiPlus } from 'react-icons/pi';
@@ -16,18 +22,38 @@ export const CanvasInstanceManager = memo(({ maxCanvases = 3 }: CanvasInstanceMa
const addCanvas = useCallback(() => {
if (canvasCount >= maxCanvases) {
return;
}
return;
}
const canvasId = nanoid();
const canvasName = `Canvas ${canvasCount + 1}`;
// For now, just add to Redux. The dockview panel creation will be handled
// by other parts of the system that have access to the dockview API
// Add to Redux first
dispatch(canvasInstanceAdded({ canvasId, name: canvasName }));
// TODO: Trigger panel creation through a global event or state change
// that the dockview can listen to
// Get the dockview API and create the panel
const dockviewApi = navigationApi.getDockviewApi('canvas', 'main');
if (dockviewApi) {
const panelId = `${WORKSPACE_PANEL_ID}_${canvasId}`;
// Create the dockview panel
dockviewApi.addPanel<DockviewPanelParameters>({
id: panelId,
component: WORKSPACE_PANEL_ID,
title: canvasName,
tabComponent: DOCKVIEW_TAB_CANVAS_WORKSPACE_ID,
params: {
tab: 'canvas',
canvasId,
focusRegion: 'canvas',
i18nKey: 'ui.panels.canvas',
},
});
// Activate the new panel
const newPanel = dockviewApi.getPanel(panelId);
newPanel?.api.setActive();
}
}, [canvasCount, maxCanvases, dispatch]);
const canCanAddCanvas = canvasCount < maxCanvases;

View File

@@ -60,6 +60,11 @@ export class NavigationApi {
*/
private panels: Map<string, PanelType> = new Map();
/**
* Map of registered containers (DockviewApi/GridviewApi), keyed by tab and container ID
*/
private containers: Map<string, DockviewApi | GridviewApi> = new Map();
/**
* Map of waiters for panel registration.
*/
@@ -219,6 +224,9 @@ export class NavigationApi {
}
const key = this._getContainerKey(tab, id);
// Store the container API for later access
this.containers.set(key, api);
const stored = this._app.storage.get(key);
if (stored) {
@@ -707,19 +715,49 @@ export class NavigationApi {
.map((key) => key.substring(prefix.length));
};
/**
* Get a registered container API by tab and container ID.
* @param tab - The tab the container belongs to
* @param id - The container ID
* @returns The DockviewApi or GridviewApi instance, or undefined if not found
*/
getContainer = (tab: TabName, id: string): DockviewApi | GridviewApi | undefined => {
const key = this._getContainerKey(tab, id);
return this.containers.get(key);
};
/**
* Get a registered DockviewApi by tab and container ID.
* @param tab - The tab the container belongs to
* @param id - The container ID
* @returns The DockviewApi instance, or undefined if not found or not a DockviewApi
*/
getDockviewApi = (tab: TabName, id: string): DockviewApi | undefined => {
const container = this.getContainer(tab, id);
return container instanceof DockviewApi ? container : undefined;
};
/**
* Unregister all panels for a tab. Any pending waiters for these panels will be rejected.
* @param tab - The tab to unregister panels for
*/
unregisterTab = (tab: TabName): void => {
const prefix = this._getPanelPrefix(tab);
const keysToDelete = Array.from(this.panels.keys()).filter((key) => key.startsWith(prefix));
const panelPrefix = this._getPanelPrefix(tab);
const panelKeysToDelete = Array.from(this.panels.keys()).filter((key) => key.startsWith(panelPrefix));
for (const key of keysToDelete) {
for (const key of panelKeysToDelete) {
this.panels.delete(key);
}
const promiseKeysToDelete = Array.from(this.waiters.keys()).filter((key) => key.startsWith(prefix));
// Clean up containers for this tab
const containerPrefix = this._getContainerPrefix(tab);
const containerKeysToDelete = Array.from(this.containers.keys()).filter((key) => key.startsWith(containerPrefix));
for (const key of containerKeysToDelete) {
this.containers.delete(key);
}
const promiseKeysToDelete = Array.from(this.waiters.keys()).filter((key) => key.startsWith(panelPrefix));
for (const key of promiseKeysToDelete) {
const waiter = this.waiters.get(key);
if (waiter) {