mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 18:25:28 -05:00
feat(ui): enforce loader when switching tabs
This commit is contained in:
@@ -17,6 +17,15 @@ module.exports = {
|
||||
'no-promise-executor-return': 'error',
|
||||
// https://eslint.org/docs/latest/rules/require-await
|
||||
'require-await': 'error',
|
||||
// Restrict setActiveTab calls to only use-navigation-api.tsx
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
selector: 'CallExpression[callee.name="setActiveTab"]',
|
||||
message:
|
||||
'setActiveTab() can only be called from use-navigation-api.tsx. Use navigationApi.switchToTab() instead.',
|
||||
},
|
||||
],
|
||||
// TODO: ENABLE THIS RULE BEFORE v6.0.0
|
||||
'react/display-name': 'off',
|
||||
'no-restricted-properties': [
|
||||
@@ -56,6 +65,15 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
/**
|
||||
* Allow setActiveTab calls only in use-navigation-api.tsx
|
||||
*/
|
||||
{
|
||||
files: ['**/use-navigation-api.tsx'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Overrides for stories
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { PartialAppConfig } from 'app/types/invokeai';
|
||||
import { useFocusRegionWatcher } from 'common/hooks/focus';
|
||||
import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix';
|
||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
@@ -45,6 +46,7 @@ export const GlobalHookIsolator = memo(
|
||||
useSyncLoggingConfig();
|
||||
useCloseChakraTooltipsOnDragFix();
|
||||
useNavigationApi();
|
||||
useDndMonitor();
|
||||
|
||||
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
|
||||
// and/or in progress canvas sessions.
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
} from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { $isStylePresetsMenuOpen, activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
|
||||
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { atom } from 'nanostores';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
@@ -122,17 +123,17 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
);
|
||||
|
||||
const handleLoadWorkflow = useCallback(
|
||||
async (workflowId: string) => {
|
||||
(workflowId: string) => {
|
||||
// This shows a toast
|
||||
await loadWorkflowWithDialog({
|
||||
loadWorkflowWithDialog({
|
||||
type: 'library',
|
||||
data: workflowId,
|
||||
onSuccess: () => {
|
||||
store.dispatch(setActiveTab('workflows'));
|
||||
navigationApi.switchToTab('workflows');
|
||||
},
|
||||
});
|
||||
},
|
||||
[loadWorkflowWithDialog, store]
|
||||
[loadWorkflowWithDialog]
|
||||
);
|
||||
|
||||
const handleSelectStylePreset = useCallback(
|
||||
@@ -146,7 +147,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
return;
|
||||
}
|
||||
store.dispatch(activeStylePresetIdChanged(stylePresetId));
|
||||
store.dispatch(setActiveTab('canvas'));
|
||||
navigationApi.switchToTab('canvas');
|
||||
toast({
|
||||
title: t('toast.stylePresetLoaded'),
|
||||
status: 'info',
|
||||
@@ -169,20 +170,20 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
break;
|
||||
case 'workflows':
|
||||
// Go to the workflows tab
|
||||
store.dispatch(setActiveTab('workflows'));
|
||||
navigationApi.switchToTab('workflows');
|
||||
break;
|
||||
case 'upscaling':
|
||||
// Go to the upscaling tab
|
||||
store.dispatch(setActiveTab('upscaling'));
|
||||
navigationApi.switchToTab('upscaling');
|
||||
break;
|
||||
case 'viewAllWorkflows':
|
||||
// Go to the workflows tab and open the workflow library modal
|
||||
store.dispatch(setActiveTab('workflows'));
|
||||
navigationApi.switchToTab('workflows');
|
||||
$isWorkflowLibraryModalOpen.set(true);
|
||||
break;
|
||||
case 'viewAllWorkflowsRecommended':
|
||||
// Go to the workflows tab and open the workflow library modal with the recommended workflows view
|
||||
store.dispatch(setActiveTab('workflows'));
|
||||
navigationApi.switchToTab('workflows');
|
||||
$isWorkflowLibraryModalOpen.set(true);
|
||||
store.dispatch(workflowLibraryViewChanged('defaults'));
|
||||
store.dispatch(workflowLibraryTagsReset());
|
||||
@@ -194,7 +195,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
|
||||
break;
|
||||
case 'viewAllStylePresets':
|
||||
// Go to the canvas tab and open the style presets menu
|
||||
store.dispatch(setActiveTab('canvas'));
|
||||
navigationApi.switchToTab('canvas');
|
||||
$isStylePresetsMenuOpen.set(true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrent
|
||||
import { useInvoke } from 'features/queue/hooks/useInvoke';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
|
||||
import { getFocusedRegion } from './focus';
|
||||
|
||||
@@ -69,7 +69,7 @@ export const useGlobalHotkeys = () => {
|
||||
id: 'selectGenerateTab',
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('generate'));
|
||||
navigationApi.switchToTab('generate');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -78,7 +78,7 @@ export const useGlobalHotkeys = () => {
|
||||
id: 'selectCanvasTab',
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('canvas'));
|
||||
navigationApi.switchToTab('canvas');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -87,7 +87,7 @@ export const useGlobalHotkeys = () => {
|
||||
id: 'selectUpscalingTab',
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('upscaling'));
|
||||
navigationApi.switchToTab('upscaling');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -96,7 +96,7 @@ export const useGlobalHotkeys = () => {
|
||||
id: 'selectWorkflowsTab',
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('workflows'));
|
||||
navigationApi.switchToTab('workflows');
|
||||
},
|
||||
dependencies: [dispatch],
|
||||
});
|
||||
@@ -105,7 +105,7 @@ export const useGlobalHotkeys = () => {
|
||||
id: 'selectModelsTab',
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('models'));
|
||||
navigationApi.switchToTab('models');
|
||||
},
|
||||
options: {
|
||||
enabled: isModelManagerEnabled,
|
||||
@@ -117,7 +117,7 @@ export const useGlobalHotkeys = () => {
|
||||
id: 'selectQueueTab',
|
||||
category: 'app',
|
||||
callback: () => {
|
||||
dispatch(setActiveTab('queue'));
|
||||
navigationApi.switchToTab('queue');
|
||||
},
|
||||
dependencies: [dispatch, isModelManagerEnabled],
|
||||
});
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { InitialStateMainModelPicker } from 'features/controlLayers/components/SimpleSession/InitialStateMainModelPicker';
|
||||
import { LaunchpadAddStyleReference } from 'features/controlLayers/components/SimpleSession/LaunchpadAddStyleReference';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
|
||||
|
||||
export const GenerateLaunchpadPanel = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const newCanvasSession = useCallback(() => {
|
||||
dispatch(setActiveTab('canvas'));
|
||||
}, [dispatch]);
|
||||
navigationApi.switchToTab('canvas');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>
|
||||
|
||||
@@ -20,8 +20,8 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
|
||||
const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
await newCanvasFromImage({ imageDTO, withResize: false, type: 'raster_layer', dispatch, getState });
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -31,8 +31,8 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
|
||||
const onClickNewCanvasWithControlLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
await newCanvasFromImage({ imageDTO, withResize: false, type: 'control_layer', dispatch, getState });
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -42,8 +42,8 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
|
||||
const onClickNewCanvasWithRasterLayerFromImageWithResize = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
await newCanvasFromImage({ imageDTO, withResize: true, type: 'raster_layer', dispatch, getState });
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -53,8 +53,8 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
|
||||
|
||||
const onClickNewCanvasWithControlLayerFromImageWithResize = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
await newCanvasFromImage({ imageDTO, withResize: true, type: 'control_layer', dispatch, getState });
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
|
||||
@@ -20,11 +20,11 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
const imageDTO = useImageDTOContext();
|
||||
const isBusy = useCanvasIsBusySafe();
|
||||
|
||||
const onClickNewRasterLayerFromImage = useCallback(() => {
|
||||
const onClickNewRasterLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'raster_layer', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -32,11 +32,11 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewControlLayerFromImage = useCallback(() => {
|
||||
const onClickNewControlLayerFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'control_layer', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -44,11 +44,11 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewInpaintMaskFromImage = useCallback(() => {
|
||||
const onClickNewInpaintMaskFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'inpaint_mask', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -56,11 +56,11 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewRegionalGuidanceFromImage = useCallback(() => {
|
||||
const onClickNewRegionalGuidanceFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
@@ -68,11 +68,11 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
|
||||
});
|
||||
}, [imageDTO, store, t]);
|
||||
|
||||
const onClickNewRegionalReferenceImageFromImage = useCallback(() => {
|
||||
const onClickNewRegionalReferenceImageFromImage = useCallback(async () => {
|
||||
const { dispatch, getState } = store;
|
||||
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance_with_reference_image', dispatch, getState });
|
||||
dispatch(sentImageToCanvas());
|
||||
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToCanvas'),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiShareFatBold } from 'react-icons/pi';
|
||||
@@ -15,7 +15,7 @@ export const ImageMenuItemSendToUpscale = memo(() => {
|
||||
|
||||
const handleSendToCanvas = useCallback(() => {
|
||||
dispatch(upscaleInitialImageChanged(imageDTO));
|
||||
dispatch(setActiveTab('upscaling'));
|
||||
navigationApi.switchToTab('upscaling');
|
||||
toast({
|
||||
id: 'SENT_TO_CANVAS',
|
||||
title: t('toast.sentToUpscale'),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { ButtonProps } from '@invoke-ai/ui-library';
|
||||
import { Alert, AlertDescription, AlertIcon, Button, Divider, Flex, Link, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { InvokeLogoIcon } from 'common/components/InvokeLogoIcon';
|
||||
import { LOADING_SYMBOL, useHasImages } from 'features/gallery/hooks/useHasImages';
|
||||
import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectIsLocal } from 'features/system/store/configSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
@@ -129,17 +129,15 @@ const GettingStartedVideosCallout = () => {
|
||||
};
|
||||
|
||||
const StarterBundlesCallout = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClickDownloadStarterModels = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
navigationApi.switchToTab('models');
|
||||
setInstallModelsTabByName('starterModels');
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
|
||||
const handleClickImportModels = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
navigationApi.switchToTab('models');
|
||||
setInstallModelsTabByName('urlOrLocal');
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Text fontSize="md" color="base.200">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Button, Text, useToast } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMainModels } from 'services/api/hooks/modelsByType';
|
||||
@@ -40,14 +39,13 @@ export const useStarterModelsToast = () => {
|
||||
|
||||
const ToastDescription = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
navigationApi.switchToTab('models');
|
||||
setInstallModelsTabByName('launchpad');
|
||||
toast.close(TOAST_ID);
|
||||
}, [dispatch, toast]);
|
||||
}, [toast]);
|
||||
|
||||
return (
|
||||
<Text fontSize="md">
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectWorkflowMode, workflowModeChanged } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { MouseEventHandler } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -18,7 +17,6 @@ export const WorkflowViewEditToggleButton = memo(() => {
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
// Navigate to workflows tab and focus the Workflow Editor panel
|
||||
dispatch(setActiveTab('workflows'));
|
||||
dispatch(workflowModeChanged('edit'));
|
||||
// Focus the Workflow Editor panel
|
||||
navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
|
||||
@@ -30,7 +28,6 @@ export const WorkflowViewEditToggleButton = memo(() => {
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
// Navigate to workflows tab and focus the Image Viewer panel
|
||||
dispatch(setActiveTab('workflows'));
|
||||
dispatch(workflowModeChanged('view'));
|
||||
// Focus the Image Viewer panel
|
||||
navigationApi.focusPanel('workflows', VIEWER_PANEL_ID);
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { IconButtonProps } from '@invoke-ai/ui-library';
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $onClickGoToModelManager } from 'app/store/nanostores/onClickGoToModelManager';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsModelsTabDisabled } from 'features/system/store/configSlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCubeBold } from 'react-icons/pi';
|
||||
@@ -14,11 +14,10 @@ export const NavigateToModelManagerButton = memo((props: Omit<IconButtonProps, '
|
||||
const onClickGoToModelManager = useStore($onClickGoToModelManager);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
}, [dispatch]);
|
||||
navigationApi.switchToTab('models');
|
||||
}, []);
|
||||
|
||||
if (isModelsTabDisabled && !onClickGoToModelManager) {
|
||||
return null;
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { $onClickGoToModelManager } from 'app/store/nanostores/onClickGoToModelManager';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { Group, PickerContextState } from 'common/components/Picker/Picker';
|
||||
import { buildGroup, getRegex, isOption, Picker, usePickerContext } from 'common/components/Picker/Picker';
|
||||
import { useDisclosure } from 'common/hooks/useBoolean';
|
||||
@@ -31,7 +31,7 @@ import { NavigateToModelManagerButton } from 'features/parameters/components/Mai
|
||||
import { API_BASE_MODELS, MODEL_TYPE_MAP, MODEL_TYPE_SHORT_MAP } from 'features/parameters/types/constants';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { selectIsModelsTabDisabled } from 'features/system/store/configSlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { filesize } from 'filesize';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
@@ -72,11 +72,10 @@ const getOptionId = <T extends AnyModelConfig>(modelConfig: WithStarred<T>) => m
|
||||
|
||||
const ModelManagerLink = memo((props: ButtonProps) => {
|
||||
const onClickGoToModelManager = useStore($onClickGoToModelManager);
|
||||
const dispatch = useAppDispatch();
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
navigationApi.switchToTab('models');
|
||||
setInstallModelsTabByName('launchpad');
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -15,7 +15,7 @@ import { setInstallModelsTabByName } from 'features/modelManagerV2/store/install
|
||||
import ParamPostProcessingModel from 'features/parameters/components/PostProcessing/ParamPostProcessingModel';
|
||||
import { selectPostProcessingModel } from 'features/parameters/store/upscaleSlice';
|
||||
import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { PiFrameCornersBold } from 'react-icons/pi';
|
||||
@@ -74,12 +74,10 @@ export const PostProcessingPopover = memo((props: Props) => {
|
||||
PostProcessingPopover.displayName = 'PostProcessingPopover';
|
||||
|
||||
const MissingModelWarning = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleGoToModelManager = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
navigationApi.switchToTab('models');
|
||||
setInstallModelsTabByName('launchpad');
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex bg="error.500" borderRadius="base" padding={4} direction="column" fontSize="sm" gap={2}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { IconButton, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { SessionMenuItems } from 'common/components/SessionMenuItems';
|
||||
import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
|
||||
import { QueueCountBadge } from 'features/queue/components/QueueCountBadge';
|
||||
@@ -7,14 +6,13 @@ import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrent
|
||||
import { usePauseProcessor } from 'features/queue/hooks/usePauseProcessor';
|
||||
import { useResumeProcessor } from 'features/queue/hooks/useResumeProcessor';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiListBold, PiPauseFill, PiPlayFill, PiQueueBold, PiXBold, PiXCircle } from 'react-icons/pi';
|
||||
|
||||
export const QueueActionsMenuButton = memo(() => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isPauseEnabled = useFeatureStatus('pauseQueue');
|
||||
const isResumeEnabled = useFeatureStatus('resumeQueue');
|
||||
@@ -23,8 +21,8 @@ export const QueueActionsMenuButton = memo(() => {
|
||||
const resumeProcessor = useResumeProcessor();
|
||||
const pauseProcessor = usePauseProcessor();
|
||||
const openQueue = useCallback(() => {
|
||||
dispatch(setActiveTab('queue'));
|
||||
}, [dispatch]);
|
||||
navigationApi.switchToTab('queue');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
tileControlnetModelChanged,
|
||||
} from 'features/parameters/store/upscaleSlice';
|
||||
import { selectIsModelsTabDisabled, selectMaxUpscaleDimension } from 'features/system/store/configSlice';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useControlNetModels } from 'services/api/hooks/modelsByType';
|
||||
@@ -68,9 +68,9 @@ export const UpscaleWarning = () => {
|
||||
const allWarnings = useMemo(() => [...modelWarnings, ...otherWarnings], [modelWarnings, otherWarnings]);
|
||||
|
||||
const handleGoToModelManager = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
navigationApi.switchToTab('models');
|
||||
setInstallModelsTabByName('launchpad');
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
|
||||
if (isBaseModelCompatible && modelWarnings.length > 0 && isModelsTabDisabled) {
|
||||
return null;
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'dockview/dist/styles/dockview.css';
|
||||
import 'features/ui/styles/dockview-theme-invoke.css';
|
||||
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import Loading from 'common/components/Loading/Loading';
|
||||
import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||
import {
|
||||
selectWithCanvasTab,
|
||||
selectWithGenerateTab,
|
||||
@@ -17,16 +17,25 @@ 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 { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
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 { memo } from 'react';
|
||||
|
||||
export const AppContent = memo(() => {
|
||||
useDndMonitor();
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" overflow="hidden">
|
||||
<VerticalNavBar />
|
||||
<TabContent />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
AppContent.displayName = 'AppContent';
|
||||
|
||||
const TabContent = memo(() => {
|
||||
const tab = useAppSelector(selectActiveTab);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const withGenerateTab = useAppSelector(selectWithGenerateTab);
|
||||
const withCanvasTab = useAppSelector(selectWithCanvasTab);
|
||||
const withUpscalingTab = useAppSelector(selectWithUpscalingTab);
|
||||
@@ -36,17 +45,25 @@ export const AppContent = memo(() => {
|
||||
|
||||
return (
|
||||
<Flex position="relative" w="full" h="full" overflow="hidden">
|
||||
<VerticalNavBar />
|
||||
<Flex position="relative" w="full" h="full" overflow="hidden">
|
||||
{withGenerateTab && tab === 'generate' && <GenerateTabAutoLayout setIsLoading={setIsLoading} />}
|
||||
{withCanvasTab && tab === 'canvas' && <CanvasTabAutoLayout setIsLoading={setIsLoading} />}
|
||||
{withUpscalingTab && tab === 'upscaling' && <UpscalingTabAutoLayout setIsLoading={setIsLoading} />}
|
||||
{withWorkflowsTab && tab === 'workflows' && <WorkflowsTabAutoLayout setIsLoading={setIsLoading} />}
|
||||
{withModelsTab && tab === 'models' && <ModelsTabAutoLayout setIsLoading={setIsLoading} />}
|
||||
{withQueueTab && tab === 'queue' && <QueueTabAutoLayout setIsLoading={setIsLoading} />}
|
||||
{isLoading && <Loading />}
|
||||
</Flex>
|
||||
{withGenerateTab && tab === 'generate' && <GenerateTabAutoLayout />}
|
||||
{withCanvasTab && tab === 'canvas' && <CanvasTabAutoLayout />}
|
||||
{withUpscalingTab && tab === 'upscaling' && <UpscalingTabAutoLayout />}
|
||||
{withWorkflowsTab && tab === 'workflows' && <WorkflowsTabAutoLayout />}
|
||||
{withModelsTab && tab === 'models' && <ModelsTabAutoLayout />}
|
||||
{withQueueTab && tab === 'queue' && <QueueTabAutoLayout />}
|
||||
<SwitchingTabsLoader />
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
AppContent.displayName = 'AppContent';
|
||||
TabContent.displayName = 'TabContent';
|
||||
|
||||
const SwitchingTabsLoader = memo(() => {
|
||||
const isSwitchingTabs = useStore(navigationApi.$isSwitchingTabs);
|
||||
|
||||
if (isSwitchingTabs) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
SwitchingTabsLoader.displayName = 'SwitchingTabsLoader';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import type { ReactElement } from 'react';
|
||||
import { memo, useCallback, useRef } from 'react';
|
||||
@@ -15,12 +15,11 @@ const sx: SystemStyleObject = {
|
||||
};
|
||||
|
||||
export const TabButton = memo(({ tab, icon, label }: { tab: TabName; icon: ReactElement; label: string }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const activeTabName = useAppSelector(selectActiveTab);
|
||||
const selectTab = useCallback(() => {
|
||||
dispatch(setActiveTab(tab));
|
||||
}, [dispatch, tab]);
|
||||
navigationApi.switchToTab(tab);
|
||||
}, [tab]);
|
||||
useCallbackOnDragEnter(selectTab, ref, 300);
|
||||
|
||||
return (
|
||||
|
||||
@@ -325,7 +325,7 @@ export const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const CanvasTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => {
|
||||
export const CanvasTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
@@ -333,22 +333,18 @@ export const CanvasTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLo
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!rootApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeRootPanelLayout(rootApi);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
navigationApi.onSwitchedTab();
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('canvas');
|
||||
};
|
||||
}, [rootApi, setIsLoading]);
|
||||
}, [rootApi]);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider tab="canvas" rootRef={rootRef}>
|
||||
|
||||
@@ -287,7 +287,7 @@ export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const GenerateTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => {
|
||||
export const GenerateTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
@@ -295,22 +295,18 @@ export const GenerateTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (is
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!rootApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeRootPanelLayout(rootApi);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
navigationApi.onSwitchedTab();
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('generate');
|
||||
};
|
||||
}, [rootApi, setIsLoading]);
|
||||
}, [rootApi]);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider tab="generate" rootRef={rootRef}>
|
||||
|
||||
@@ -24,7 +24,7 @@ export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
return { models } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const ModelsTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => {
|
||||
export const ModelsTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
@@ -32,22 +32,18 @@ export const ModelsTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLo
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!rootApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeRootPanelLayout(rootApi);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
navigationApi.onSwitchedTab();
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('models');
|
||||
};
|
||||
}, [rootApi, setIsLoading]);
|
||||
}, [rootApi]);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider tab="models" rootRef={rootRef}>
|
||||
|
||||
@@ -83,16 +83,16 @@ describe('AppNavigationApi', () => {
|
||||
it('should connect to app', () => {
|
||||
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
|
||||
|
||||
expect(navigationApi.setAppTab).toBe(mockSetAppTab);
|
||||
expect(navigationApi.getAppTab).toBe(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();
|
||||
expect(navigationApi._setAppTab).toBeNull();
|
||||
expect(navigationApi._getAppTab).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -348,8 +348,8 @@ describe('AppNavigationApi', () => {
|
||||
unregister();
|
||||
navigationApi.disconnectFromApp();
|
||||
|
||||
expect(navigationApi.setAppTab).toBeNull();
|
||||
expect(navigationApi.getAppTab).toBeNull();
|
||||
expect(navigationApi._setAppTab).toBeNull();
|
||||
expect(navigationApi._getAppTab).toBeNull();
|
||||
expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,8 +2,15 @@ import { logger } from 'app/logging/logger';
|
||||
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 { atom } from 'nanostores';
|
||||
|
||||
import { LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX } from './shared';
|
||||
import {
|
||||
LEFT_PANEL_ID,
|
||||
LEFT_PANEL_MIN_SIZE_PX,
|
||||
RIGHT_PANEL_ID,
|
||||
RIGHT_PANEL_MIN_SIZE_PX,
|
||||
SWITCH_TABS_FAKE_DELAY_MS,
|
||||
} from './shared';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
@@ -18,20 +25,49 @@ export class NavigationApi {
|
||||
private panels: Map<string, PanelType> = new Map();
|
||||
private waiters: Map<string, Waiter> = new Map();
|
||||
|
||||
$isSwitchingTabs = atom(false);
|
||||
switchingTabsTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
KEY_SEPARATOR = ':';
|
||||
|
||||
setAppTab: ((tab: TabName) => void) | null = null;
|
||||
getAppTab: (() => TabName) | null = null;
|
||||
_setAppTab: ((tab: TabName) => void) | null = null;
|
||||
_getAppTab: (() => TabName) | null = null;
|
||||
|
||||
connectToApp = (arg: { setAppTab: (tab: TabName) => void; getAppTab: () => TabName }): void => {
|
||||
const { setAppTab, getAppTab } = arg;
|
||||
this.setAppTab = setAppTab;
|
||||
this.getAppTab = getAppTab;
|
||||
this._setAppTab = setAppTab;
|
||||
this._getAppTab = getAppTab;
|
||||
};
|
||||
|
||||
disconnectFromApp = (): void => {
|
||||
this.setAppTab = null;
|
||||
this.getAppTab = null;
|
||||
this._setAppTab = null;
|
||||
this._getAppTab = null;
|
||||
};
|
||||
|
||||
switchToTab = (tab: TabName): boolean => {
|
||||
if (this.switchingTabsTimeout !== null) {
|
||||
clearTimeout(this.switchingTabsTimeout);
|
||||
this.switchingTabsTimeout = null;
|
||||
}
|
||||
if (tab === this._getAppTab?.()) {
|
||||
return true;
|
||||
}
|
||||
this.$isSwitchingTabs.set(true);
|
||||
log.debug(`Switching to tab: ${tab}`);
|
||||
if (this._setAppTab) {
|
||||
this._setAppTab(tab);
|
||||
return true;
|
||||
} else {
|
||||
log.error('No setAppTab function available to switch tabs');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
onSwitchedTab = (): void => {
|
||||
log.debug('Tab switch completed');
|
||||
this.switchingTabsTimeout = setTimeout(() => {
|
||||
this.$isSwitchingTabs.set(false);
|
||||
}, SWITCH_TABS_FAKE_DELAY_MS);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -118,9 +154,7 @@ export class NavigationApi {
|
||||
focusPanel = async (tab: TabName, panelId: string): Promise<boolean> => {
|
||||
try {
|
||||
// Switch to the target tab if needed
|
||||
if (this.setAppTab && this.getAppTab && this.getAppTab() !== tab) {
|
||||
this.setAppTab(tab);
|
||||
}
|
||||
this.switchToTab(tab);
|
||||
|
||||
// Wait for the panel to be ready
|
||||
await this.waitForPanel(tab, panelId);
|
||||
@@ -145,7 +179,7 @@ export class NavigationApi {
|
||||
};
|
||||
|
||||
focusPanelInActiveTab = (panelId: string): Promise<boolean> => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
const activeTab = this._getAppTab ? this._getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.error('No active tab found');
|
||||
return Promise.resolve(false);
|
||||
@@ -169,7 +203,7 @@ export class NavigationApi {
|
||||
};
|
||||
|
||||
toggleLeftPanel = (): boolean => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
const activeTab = this._getAppTab ? this._getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.warn('No active tab found to toggle left panel');
|
||||
return false;
|
||||
@@ -195,7 +229,7 @@ export class NavigationApi {
|
||||
};
|
||||
|
||||
toggleRightPanel = (): boolean => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
const activeTab = this._getAppTab ? this._getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.warn('No active tab found to toggle right panel');
|
||||
return false;
|
||||
@@ -221,7 +255,7 @@ export class NavigationApi {
|
||||
};
|
||||
|
||||
toggleLeftAndRightPanels = (): boolean => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
const activeTab = this._getAppTab ? this._getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.warn('No active tab found to toggle right panel');
|
||||
return false;
|
||||
@@ -256,7 +290,7 @@ export class NavigationApi {
|
||||
* Reset panels in a specific tab (expand both left and right)
|
||||
*/
|
||||
resetLeftAndRightPanels = (): boolean => {
|
||||
const activeTab = this.getAppTab ? this.getAppTab() : null;
|
||||
const activeTab = this._getAppTab ? this._getAppTab() : null;
|
||||
if (!activeTab) {
|
||||
log.warn('No active tab found to toggle right panel');
|
||||
return false;
|
||||
|
||||
@@ -24,7 +24,7 @@ export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
return { queue } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const QueueTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => {
|
||||
export const QueueTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
@@ -32,22 +32,18 @@ export const QueueTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoa
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!rootApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeRootPanelLayout(rootApi);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
navigationApi.onSwitchedTab();
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('queue');
|
||||
};
|
||||
}, [rootApi, setIsLoading]);
|
||||
}, [rootApi]);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider tab="queue" rootRef={rootRef}>
|
||||
|
||||
@@ -34,3 +34,5 @@ export const LAYERS_PANEL_DEFAULT_HEIGHT_PX = 232;
|
||||
|
||||
export const CANVAS_BOARD_PANEL_DEFAULT_HEIGHT_PX = 36; // Collapsed by default on Canvas
|
||||
export const CANVAS_GALLERY_PANEL_DEFAULT_HEIGHT_PX = 200; // Smaller default size on Canvas
|
||||
|
||||
export const SWITCH_TABS_FAKE_DELAY_MS = 300;
|
||||
|
||||
@@ -287,7 +287,7 @@ export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const UpscalingTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => {
|
||||
export const UpscalingTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
@@ -295,21 +295,18 @@ export const UpscalingTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (i
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!rootApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeRootPanelLayout(rootApi);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
navigationApi.onSwitchedTab();
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('upscaling');
|
||||
};
|
||||
}, [rootApi, setIsLoading]);
|
||||
}, [rootApi]);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider tab="upscaling" rootRef={rootRef}>
|
||||
|
||||
@@ -304,7 +304,7 @@ export const initializeRootPanelLayout = (api: GridviewApi) => {
|
||||
return { main, left, right } satisfies Record<string, IGridviewPanel>;
|
||||
};
|
||||
|
||||
export const WorkflowsTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => {
|
||||
export const WorkflowsTabAutoLayout = memo(() => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [rootApi, setRootApi] = useState<GridviewApi | null>(null);
|
||||
const onReady = useCallback<IGridviewReactProps['onReady']>(({ api }) => {
|
||||
@@ -312,22 +312,18 @@ export const WorkflowsTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (i
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!rootApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
initializeRootPanelLayout(rootApi);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 300);
|
||||
navigationApi.onSwitchedTab();
|
||||
|
||||
return () => {
|
||||
navigationApi.unregisterTab('workflows');
|
||||
};
|
||||
}, [rootApi, setIsLoading]);
|
||||
}, [rootApi]);
|
||||
|
||||
return (
|
||||
<AutoLayoutProvider tab="workflows" rootRef={rootRef}>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { canvasReset } from 'features/controlLayers/store/actions';
|
||||
import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
|
||||
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
import type { TabName, UIState } from './uiTypes';
|
||||
@@ -65,23 +62,6 @@ export const uiSlice = createSlice({
|
||||
state.showCanvasTabSplashScreen = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addCase(workflowLoaded, (state) => {
|
||||
state.activeTab = 'workflows';
|
||||
});
|
||||
builder.addCase(canvasReset, (state) => {
|
||||
state.activeTab = 'canvas';
|
||||
});
|
||||
builder.addCase(canvasSessionReset, (state) => {
|
||||
state.activeTab = 'canvas';
|
||||
});
|
||||
builder.addCase(generateSessionReset, (state) => {
|
||||
state.activeTab = 'generate';
|
||||
});
|
||||
// builder.addCase(canvasSessionTypeChanged, (state) => {
|
||||
// state.activeTab = 'canvas';
|
||||
// });
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/typ
|
||||
import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { serializeError } from 'serialize-error';
|
||||
@@ -47,6 +49,7 @@ export const useValidateAndLoadWorkflow = () => {
|
||||
origin: 'file' | 'image' | 'object' | 'library'
|
||||
): Promise<WorkflowV3 | null> => {
|
||||
try {
|
||||
await navigationApi.focusPanel('workflows', WORKSPACE_PANEL_ID);
|
||||
const templates = $templates.get();
|
||||
const { workflow, warnings } = await validateWorkflow({
|
||||
workflow: unvalidatedWorkflow,
|
||||
|
||||
@@ -2,11 +2,10 @@ import { Button, ExternalLink, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { AppDispatch, AppGetState } from 'app/store/store';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { toast, toastApi } from 'features/toast/toast';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { t } from 'i18next';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -150,12 +149,11 @@ const HFUnauthorizedToastDescription = () => {
|
||||
const { data } = useGetHFTokenStatusQuery(isEnabled ? undefined : skipToken);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(setActiveTab('models'));
|
||||
navigationApi.switchToTab('models');
|
||||
toastApi.close(UNAUTHORIZED_TOAST_ID);
|
||||
}, [dispatch]);
|
||||
}, []);
|
||||
|
||||
if (!data) {
|
||||
return <Spinner />;
|
||||
|
||||
Reference in New Issue
Block a user