fix(ui): really do not load disabled tabs

Ensure disabled tabs are never mounted:
- Add didLoad flag to configSlice, default false
- Always merge in config - even it is is empty
- On first merge, set didLoad to true
- Until didLoad is true, mark _all_ tabs as disabled

This gets around an issue where tabs are all enabled for a brief moment
before the config is loaded.

A bit hacky but it works.
This commit is contained in:
psychedelicious
2025-07-02 00:04:10 +10:00
committed by Mary Hipp Rogers
parent 79fea1ac40
commit 3f78ac9295
4 changed files with 75 additions and 40 deletions

View File

@@ -10,7 +10,6 @@ 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 { size } from 'es-toolkit/compat';
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
@@ -55,10 +54,8 @@ export const GlobalHookIsolator = memo(
}, [language]);
useEffect(() => {
if (size(config)) {
logger.info({ config }, 'Received config');
dispatch(configChanged(config));
}
logger.info({ config }, 'Received config');
dispatch(configChanged(config));
}, [dispatch, config, logger]);
useEffect(() => {

View File

@@ -16,7 +16,8 @@ const baseDimensionConfig: NumericalParameterConfig = {
coarseStep: 64,
};
const initialConfigState: AppConfig = {
const initialConfigState: AppConfig & { didLoad: boolean } = {
didLoad: false,
isLocal: true,
shouldUpdateImagesOnConnect: false,
shouldFetchMetadataFromApi: false,
@@ -191,6 +192,7 @@ export const configSlice = createSlice({
reducers: {
configChanged: (state, action: PayloadAction<PartialAppConfig>) => {
merge(state, action.payload);
state.didLoad = true;
},
},
});
@@ -198,7 +200,8 @@ export const configSlice = createSlice({
export const { configChanged } = configSlice.actions;
export const selectConfigSlice = (state: RootState) => state.config;
const createConfigSelector = <T>(selector: Selector<AppConfig, T>) => createSelector(selectConfigSlice, selector);
const createConfigSelector = <T>(selector: Selector<typeof initialConfigState, T>) =>
createSelector(selectConfigSlice, selector);
export const selectWidthConfig = createConfigSelector((config) => config.sd.width);
export const selectHeightConfig = createConfigSelector((config) => config.sd.height);
@@ -236,3 +239,23 @@ export const selectEnabledTabs = createConfigSelector((config) => {
}
return enabledTabs;
});
const selectDisabledTabs = createConfigSelector((config) => config.disabledTabs);
const selectDidLoad = createConfigSelector((config) => config.didLoad);
export const selectWithGenerateTab = createSelector(selectDidLoad, selectDisabledTabs, (didLoad, disabledTabs) =>
didLoad ? !disabledTabs.includes('generate') : false
);
export const selectWithCanvasTab = createSelector(selectDidLoad, selectDisabledTabs, (didLoad, disabledTabs) =>
didLoad ? !disabledTabs.includes('canvas') : false
);
export const selectWithUpscalingTab = createSelector(selectDidLoad, selectDisabledTabs, (didLoad, disabledTabs) =>
didLoad ? !disabledTabs.includes('upscaling') : false
);
export const selectWithWorkflowsTab = createSelector(selectDidLoad, selectDisabledTabs, (didLoad, disabledTabs) =>
didLoad ? !disabledTabs.includes('workflows') : false
);
export const selectWithModelsTab = createSelector(selectDidLoad, selectDisabledTabs, (didLoad, disabledTabs) =>
didLoad ? !disabledTabs.includes('models') : false
);
export const selectWithQueueTab = createSelector(selectDidLoad, selectDisabledTabs, (didLoad, disabledTabs) =>
didLoad ? !disabledTabs.includes('queue') : false
);

View File

@@ -4,6 +4,14 @@ import 'features/ui/styles/dockview-theme-invoke.css';
import { TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useDndMonitor } from 'features/dnd/useDndMonitor';
import {
selectWithCanvasTab,
selectWithGenerateTab,
selectWithModelsTab,
selectWithQueueTab,
selectWithUpscalingTab,
selectWithWorkflowsTab,
} from 'features/system/store/configSlice';
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';
@@ -12,48 +20,53 @@ import { WorkflowsTabAutoLayout } from 'features/ui/layouts/workflows-tab-auto-l
import { selectActiveTabIndex } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { TabMountGate } from './TabMountGate';
import ModelManagerTab from './tabs/ModelManagerTab';
import QueueTab from './tabs/QueueTab';
export const AppContent = memo(() => {
const tabIndex = useAppSelector(selectActiveTabIndex);
useDndMonitor();
const tabIndex = useAppSelector(selectActiveTabIndex);
const withGenerateTab = useAppSelector(selectWithGenerateTab);
const withCanvasTab = useAppSelector(selectWithCanvasTab);
const withUpscalingTab = useAppSelector(selectWithUpscalingTab);
const withWorkflowsTab = useAppSelector(selectWithWorkflowsTab);
const withModelsTab = useAppSelector(selectWithModelsTab);
const withQueueTab = useAppSelector(selectWithQueueTab);
return (
<Tabs index={tabIndex} display="flex" w="full" h="full" p={0} overflow="hidden">
<VerticalNavBar />
<TabPanels w="full" h="full" p={0}>
<TabMountGate tab="generate">
{withGenerateTab && (
<TabPanel w="full" h="full" p={0}>
<GenerateTabAutoLayout />
</TabPanel>
</TabMountGate>
<TabMountGate tab="canvas">
)}
{withCanvasTab && (
<TabPanel w="full" h="full" p={0}>
<CanvasTabAutoLayout />
</TabPanel>
</TabMountGate>
<TabMountGate tab="upscaling">
)}
{withUpscalingTab && (
<TabPanel w="full" h="full" p={0}>
<UpscalingTabAutoLayout />
</TabPanel>
</TabMountGate>
<TabMountGate tab="workflows">
)}
{withWorkflowsTab && (
<TabPanel w="full" h="full" p={0}>
<WorkflowsTabAutoLayout />
</TabPanel>
</TabMountGate>
<TabMountGate tab="models">
)}
{withModelsTab && (
<TabPanel w="full" h="full" p={0}>
<ModelManagerTab />
</TabPanel>
</TabMountGate>
<TabMountGate tab="queue">
)}
{withQueueTab && (
<TabPanel w="full" h="full" p={0}>
<QueueTab />
</TabPanel>
</TabMountGate>
)}
</TabPanels>
</Tabs>
);

View File

@@ -1,11 +1,19 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
import { useAppSelector } from 'app/store/storeHooks';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
import StatusIndicator from 'features/system/components/StatusIndicator';
import { VideosModalButton } from 'features/system/components/VideosModal/VideosModalButton';
import { TabMountGate } from 'features/ui/components/TabMountGate';
import {
selectWithCanvasTab,
selectWithGenerateTab,
selectWithModelsTab,
selectWithQueueTab,
selectWithUpscalingTab,
selectWithWorkflowsTab,
} from 'features/system/store/configSlice';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -23,29 +31,23 @@ import { TabButton } from './TabButton';
export const VerticalNavBar = memo(() => {
const { t } = useTranslation();
const customNavComponent = useStore($customNavComponent);
const withGenerateTab = useAppSelector(selectWithGenerateTab);
const withCanvasTab = useAppSelector(selectWithCanvasTab);
const withUpscalingTab = useAppSelector(selectWithUpscalingTab);
const withWorkflowsTab = useAppSelector(selectWithWorkflowsTab);
const withModelsTab = useAppSelector(selectWithModelsTab);
const withQueueTab = useAppSelector(selectWithQueueTab);
return (
<Flex flexDir="column" alignItems="center" py={6} ps={4} pe={2} gap={4} minW={0} flexShrink={0}>
<InvokeAILogoComponent />
<Flex gap={6} pt={6} h="full" flexDir="column">
<TabMountGate tab="generate">
<TabButton tab="generate" icon={<PiTextAaBold />} label="Generate" />
</TabMountGate>
<TabMountGate tab="canvas">
<TabButton tab="canvas" icon={<PiBoundingBoxBold />} label={t('ui.tabs.canvas')} />
</TabMountGate>
<TabMountGate tab="upscaling">
<TabButton tab="upscaling" icon={<PiFrameCornersBold />} label={t('ui.tabs.upscaling')} />
</TabMountGate>
<TabMountGate tab="workflows">
<TabButton tab="workflows" icon={<PiFlowArrowBold />} label={t('ui.tabs.workflows')} />
</TabMountGate>
<TabMountGate tab="models">
<TabButton tab="models" icon={<PiCubeBold />} label={t('ui.tabs.models')} />
</TabMountGate>
<TabMountGate tab="queue">
<TabButton tab="queue" icon={<PiQueueBold />} label={t('ui.tabs.queue')} />
</TabMountGate>
{withGenerateTab && <TabButton tab="generate" icon={<PiTextAaBold />} label="Generate" />}
{withCanvasTab && <TabButton tab="canvas" icon={<PiBoundingBoxBold />} label={t('ui.tabs.canvas')} />}
{withUpscalingTab && <TabButton tab="upscaling" icon={<PiFrameCornersBold />} label={t('ui.tabs.upscaling')} />}
{withWorkflowsTab && <TabButton tab="workflows" icon={<PiFlowArrowBold />} label={t('ui.tabs.workflows')} />}
{withModelsTab && <TabButton tab="models" icon={<PiCubeBold />} label={t('ui.tabs.models')} />}
{withQueueTab && <TabButton tab="queue" icon={<PiQueueBold />} label={t('ui.tabs.queue')} />}
</Flex>
<Spacer />
<StatusIndicator />