Skip to content

Commit d94aa4a

Browse files
feat(ui): enforce loader when switching tabs
1 parent 69a56aa commit d94aa4a

30 files changed

+202
-186
lines changed

invokeai/frontend/web/.eslintrc.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ module.exports = {
1717
'no-promise-executor-return': 'error',
1818
// https://eslint.org/docs/latest/rules/require-await
1919
'require-await': 'error',
20+
// Restrict setActiveTab calls to only use-navigation-api.tsx
21+
'no-restricted-syntax': [
22+
'error',
23+
{
24+
selector: 'CallExpression[callee.name="setActiveTab"]',
25+
message:
26+
'setActiveTab() can only be called from use-navigation-api.tsx. Use navigationApi.switchToTab() instead.',
27+
},
28+
],
2029
// TODO: ENABLE THIS RULE BEFORE v6.0.0
2130
'react/display-name': 'off',
2231
'no-restricted-properties': [
@@ -56,6 +65,15 @@ module.exports = {
5665
],
5766
},
5867
overrides: [
68+
/**
69+
* Allow setActiveTab calls only in use-navigation-api.tsx
70+
*/
71+
{
72+
files: ['**/use-navigation-api.tsx'],
73+
rules: {
74+
'no-restricted-syntax': 'off',
75+
},
76+
},
5977
/**
6078
* Overrides for stories
6179
*/

invokeai/frontend/web/src/app/components/GlobalHookIsolator.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { PartialAppConfig } from 'app/types/invokeai';
1111
import { useFocusRegionWatcher } from 'common/hooks/focus';
1212
import { useCloseChakraTooltipsOnDragFix } from 'common/hooks/useCloseChakraTooltipsOnDragFix';
1313
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
14+
import { useDndMonitor } from 'features/dnd/useDndMonitor';
1415
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
1516
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
1617
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
@@ -45,6 +46,7 @@ export const GlobalHookIsolator = memo(
4546
useSyncLoggingConfig();
4647
useCloseChakraTooltipsOnDragFix();
4748
useNavigationApi();
49+
useDndMonitor();
4850

4951
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
5052
// and/or in progress canvas sessions.

invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
} from 'features/nodes/store/workflowLibrarySlice';
2020
import { $isStylePresetsMenuOpen, activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
2121
import { toast } from 'features/toast/toast';
22-
import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice';
22+
import { navigationApi } from 'features/ui/layouts/navigation-api';
23+
import { activeTabCanvasRightPanelChanged } from 'features/ui/store/uiSlice';
2324
import { useLoadWorkflowWithDialog } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
2425
import { atom } from 'nanostores';
2526
import { useCallback, useEffect } from 'react';
@@ -122,17 +123,17 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
122123
);
123124

124125
const handleLoadWorkflow = useCallback(
125-
async (workflowId: string) => {
126+
(workflowId: string) => {
126127
// This shows a toast
127-
await loadWorkflowWithDialog({
128+
loadWorkflowWithDialog({
128129
type: 'library',
129130
data: workflowId,
130131
onSuccess: () => {
131-
store.dispatch(setActiveTab('workflows'));
132+
navigationApi.switchToTab('workflows');
132133
},
133134
});
134135
},
135-
[loadWorkflowWithDialog, store]
136+
[loadWorkflowWithDialog]
136137
);
137138

138139
const handleSelectStylePreset = useCallback(
@@ -146,7 +147,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
146147
return;
147148
}
148149
store.dispatch(activeStylePresetIdChanged(stylePresetId));
149-
store.dispatch(setActiveTab('canvas'));
150+
navigationApi.switchToTab('canvas');
150151
toast({
151152
title: t('toast.stylePresetLoaded'),
152153
status: 'info',
@@ -169,20 +170,20 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
169170
break;
170171
case 'workflows':
171172
// Go to the workflows tab
172-
store.dispatch(setActiveTab('workflows'));
173+
navigationApi.switchToTab('workflows');
173174
break;
174175
case 'upscaling':
175176
// Go to the upscaling tab
176-
store.dispatch(setActiveTab('upscaling'));
177+
navigationApi.switchToTab('upscaling');
177178
break;
178179
case 'viewAllWorkflows':
179180
// Go to the workflows tab and open the workflow library modal
180-
store.dispatch(setActiveTab('workflows'));
181+
navigationApi.switchToTab('workflows');
181182
$isWorkflowLibraryModalOpen.set(true);
182183
break;
183184
case 'viewAllWorkflowsRecommended':
184185
// Go to the workflows tab and open the workflow library modal with the recommended workflows view
185-
store.dispatch(setActiveTab('workflows'));
186+
navigationApi.switchToTab('workflows');
186187
$isWorkflowLibraryModalOpen.set(true);
187188
store.dispatch(workflowLibraryViewChanged('defaults'));
188189
store.dispatch(workflowLibraryTagsReset());
@@ -194,7 +195,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
194195
break;
195196
case 'viewAllStylePresets':
196197
// Go to the canvas tab and open the style presets menu
197-
store.dispatch(setActiveTab('canvas'));
198+
navigationApi.switchToTab('canvas');
198199
$isStylePresetsMenuOpen.set(true);
199200
break;
200201
}

invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useDeleteCurrentQueueItem } from 'features/queue/hooks/useDeleteCurrent
66
import { useInvoke } from 'features/queue/hooks/useInvoke';
77
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
88
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
9-
import { setActiveTab } from 'features/ui/store/uiSlice';
9+
import { navigationApi } from 'features/ui/layouts/navigation-api';
1010

1111
import { getFocusedRegion } from './focus';
1212

@@ -69,7 +69,7 @@ export const useGlobalHotkeys = () => {
6969
id: 'selectGenerateTab',
7070
category: 'app',
7171
callback: () => {
72-
dispatch(setActiveTab('generate'));
72+
navigationApi.switchToTab('generate');
7373
},
7474
dependencies: [dispatch],
7575
});
@@ -78,7 +78,7 @@ export const useGlobalHotkeys = () => {
7878
id: 'selectCanvasTab',
7979
category: 'app',
8080
callback: () => {
81-
dispatch(setActiveTab('canvas'));
81+
navigationApi.switchToTab('canvas');
8282
},
8383
dependencies: [dispatch],
8484
});
@@ -87,7 +87,7 @@ export const useGlobalHotkeys = () => {
8787
id: 'selectUpscalingTab',
8888
category: 'app',
8989
callback: () => {
90-
dispatch(setActiveTab('upscaling'));
90+
navigationApi.switchToTab('upscaling');
9191
},
9292
dependencies: [dispatch],
9393
});
@@ -96,7 +96,7 @@ export const useGlobalHotkeys = () => {
9696
id: 'selectWorkflowsTab',
9797
category: 'app',
9898
callback: () => {
99-
dispatch(setActiveTab('workflows'));
99+
navigationApi.switchToTab('workflows');
100100
},
101101
dependencies: [dispatch],
102102
});
@@ -105,7 +105,7 @@ export const useGlobalHotkeys = () => {
105105
id: 'selectModelsTab',
106106
category: 'app',
107107
callback: () => {
108-
dispatch(setActiveTab('models'));
108+
navigationApi.switchToTab('models');
109109
},
110110
options: {
111111
enabled: isModelManagerEnabled,
@@ -117,7 +117,7 @@ export const useGlobalHotkeys = () => {
117117
id: 'selectQueueTab',
118118
category: 'app',
119119
callback: () => {
120-
dispatch(setActiveTab('queue'));
120+
navigationApi.switchToTab('queue');
121121
},
122122
dependencies: [dispatch, isModelManagerEnabled],
123123
});

invokeai/frontend/web/src/features/controlLayers/components/SimpleSession/GenerateLaunchpadPanel.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import { Alert, Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
2-
import { useAppDispatch } from 'app/store/storeHooks';
32
import { InitialStateMainModelPicker } from 'features/controlLayers/components/SimpleSession/InitialStateMainModelPicker';
43
import { LaunchpadAddStyleReference } from 'features/controlLayers/components/SimpleSession/LaunchpadAddStyleReference';
5-
import { setActiveTab } from 'features/ui/store/uiSlice';
4+
import { navigationApi } from 'features/ui/layouts/navigation-api';
65
import { memo, useCallback } from 'react';
76

87
import { LaunchpadGenerateFromTextButton } from './LaunchpadGenerateFromTextButton';
98

109
export const GenerateLaunchpadPanel = memo(() => {
11-
const dispatch = useAppDispatch();
1210
const newCanvasSession = useCallback(() => {
13-
dispatch(setActiveTab('canvas'));
14-
}, [dispatch]);
11+
navigationApi.switchToTab('canvas');
12+
}, []);
1513

1614
return (
1715
<Flex flexDir="column" h="full" w="full" alignItems="center" gap={2}>

invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewCanvasFromImageSubMenu.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
2020

2121
const onClickNewCanvasWithRasterLayerFromImage = useCallback(async () => {
2222
const { dispatch, getState } = store;
23+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
2324
await newCanvasFromImage({ imageDTO, withResize: false, type: 'raster_layer', dispatch, getState });
24-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
2525
toast({
2626
id: 'SENT_TO_CANVAS',
2727
title: t('toast.sentToCanvas'),
@@ -31,8 +31,8 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
3131

3232
const onClickNewCanvasWithControlLayerFromImage = useCallback(async () => {
3333
const { dispatch, getState } = store;
34+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
3435
await newCanvasFromImage({ imageDTO, withResize: false, type: 'control_layer', dispatch, getState });
35-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
3636
toast({
3737
id: 'SENT_TO_CANVAS',
3838
title: t('toast.sentToCanvas'),
@@ -42,8 +42,8 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
4242

4343
const onClickNewCanvasWithRasterLayerFromImageWithResize = useCallback(async () => {
4444
const { dispatch, getState } = store;
45+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
4546
await newCanvasFromImage({ imageDTO, withResize: true, type: 'raster_layer', dispatch, getState });
46-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
4747
toast({
4848
id: 'SENT_TO_CANVAS',
4949
title: t('toast.sentToCanvas'),
@@ -53,8 +53,8 @@ export const ImageMenuItemNewCanvasFromImageSubMenu = memo(() => {
5353

5454
const onClickNewCanvasWithControlLayerFromImageWithResize = useCallback(async () => {
5555
const { dispatch, getState } = store;
56+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
5657
await newCanvasFromImage({ imageDTO, withResize: true, type: 'control_layer', dispatch, getState });
57-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
5858
toast({
5959
id: 'SENT_TO_CANVAS',
6060
title: t('toast.sentToCanvas'),

invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemNewLayerFromImageSubMenu.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,59 +20,59 @@ export const ImageMenuItemNewLayerFromImageSubMenu = memo(() => {
2020
const imageDTO = useImageDTOContext();
2121
const isBusy = useCanvasIsBusySafe();
2222

23-
const onClickNewRasterLayerFromImage = useCallback(() => {
23+
const onClickNewRasterLayerFromImage = useCallback(async () => {
2424
const { dispatch, getState } = store;
25+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
2526
createNewCanvasEntityFromImage({ imageDTO, type: 'raster_layer', dispatch, getState });
2627
dispatch(sentImageToCanvas());
27-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
2828
toast({
2929
id: 'SENT_TO_CANVAS',
3030
title: t('toast.sentToCanvas'),
3131
status: 'success',
3232
});
3333
}, [imageDTO, store, t]);
3434

35-
const onClickNewControlLayerFromImage = useCallback(() => {
35+
const onClickNewControlLayerFromImage = useCallback(async () => {
3636
const { dispatch, getState } = store;
37+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
3738
createNewCanvasEntityFromImage({ imageDTO, type: 'control_layer', dispatch, getState });
3839
dispatch(sentImageToCanvas());
39-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
4040
toast({
4141
id: 'SENT_TO_CANVAS',
4242
title: t('toast.sentToCanvas'),
4343
status: 'success',
4444
});
4545
}, [imageDTO, store, t]);
4646

47-
const onClickNewInpaintMaskFromImage = useCallback(() => {
47+
const onClickNewInpaintMaskFromImage = useCallback(async () => {
4848
const { dispatch, getState } = store;
49+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
4950
createNewCanvasEntityFromImage({ imageDTO, type: 'inpaint_mask', dispatch, getState });
5051
dispatch(sentImageToCanvas());
51-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
5252
toast({
5353
id: 'SENT_TO_CANVAS',
5454
title: t('toast.sentToCanvas'),
5555
status: 'success',
5656
});
5757
}, [imageDTO, store, t]);
5858

59-
const onClickNewRegionalGuidanceFromImage = useCallback(() => {
59+
const onClickNewRegionalGuidanceFromImage = useCallback(async () => {
6060
const { dispatch, getState } = store;
61+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
6162
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance', dispatch, getState });
6263
dispatch(sentImageToCanvas());
63-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
6464
toast({
6565
id: 'SENT_TO_CANVAS',
6666
title: t('toast.sentToCanvas'),
6767
status: 'success',
6868
});
6969
}, [imageDTO, store, t]);
7070

71-
const onClickNewRegionalReferenceImageFromImage = useCallback(() => {
71+
const onClickNewRegionalReferenceImageFromImage = useCallback(async () => {
7272
const { dispatch, getState } = store;
73+
await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
7374
createNewCanvasEntityFromImage({ imageDTO, type: 'regional_guidance_with_reference_image', dispatch, getState });
7475
dispatch(sentImageToCanvas());
75-
navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
7676
toast({
7777
id: 'SENT_TO_CANVAS',
7878
title: t('toast.sentToCanvas'),

invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemSendToUpscale.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks';
33
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
44
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
55
import { toast } from 'features/toast/toast';
6-
import { setActiveTab } from 'features/ui/store/uiSlice';
6+
import { navigationApi } from 'features/ui/layouts/navigation-api';
77
import { memo, useCallback } from 'react';
88
import { useTranslation } from 'react-i18next';
99
import { PiShareFatBold } from 'react-icons/pi';
@@ -15,7 +15,7 @@ export const ImageMenuItemSendToUpscale = memo(() => {
1515

1616
const handleSendToCanvas = useCallback(() => {
1717
dispatch(upscaleInitialImageChanged(imageDTO));
18-
dispatch(setActiveTab('upscaling'));
18+
navigationApi.switchToTab('upscaling');
1919
toast({
2020
id: 'SENT_TO_CANVAS',
2121
title: t('toast.sentToUpscale'),

invokeai/frontend/web/src/features/gallery/components/ImageViewer/NoContentForViewer.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type { ButtonProps } from '@invoke-ai/ui-library';
22
import { Alert, AlertDescription, AlertIcon, Button, Divider, Flex, Link, Spinner, Text } from '@invoke-ai/ui-library';
3-
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
3+
import { useAppSelector } from 'app/store/storeHooks';
44
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
55
import { InvokeLogoIcon } from 'common/components/InvokeLogoIcon';
66
import { LOADING_SYMBOL, useHasImages } from 'features/gallery/hooks/useHasImages';
77
import { setInstallModelsTabByName } from 'features/modelManagerV2/store/installModelsStore';
88
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
99
import { selectIsLocal } from 'features/system/store/configSlice';
10+
import { navigationApi } from 'features/ui/layouts/navigation-api';
1011
import { selectActiveTab } from 'features/ui/store/uiSelectors';
11-
import { setActiveTab } from 'features/ui/store/uiSlice';
1212
import type { PropsWithChildren } from 'react';
1313
import { memo, useCallback, useMemo } from 'react';
1414
import { Trans, useTranslation } from 'react-i18next';
@@ -129,17 +129,15 @@ const GettingStartedVideosCallout = () => {
129129
};
130130

131131
const StarterBundlesCallout = () => {
132-
const dispatch = useAppDispatch();
133-
134132
const handleClickDownloadStarterModels = useCallback(() => {
135-
dispatch(setActiveTab('models'));
133+
navigationApi.switchToTab('models');
136134
setInstallModelsTabByName('starterModels');
137-
}, [dispatch]);
135+
}, []);
138136

139137
const handleClickImportModels = useCallback(() => {
140-
dispatch(setActiveTab('models'));
138+
navigationApi.switchToTab('models');
141139
setInstallModelsTabByName('urlOrLocal');
142-
}, [dispatch]);
140+
}, []);
143141

144142
return (
145143
<Text fontSize="md" color="base.200">

0 commit comments

Comments
 (0)