Skip to content

Commit 7fcf475

Browse files
feat(ui): add Update All Nodes button
1 parent 3f6e8e9 commit 7fcf475

8 files changed

Lines changed: 183 additions & 46 deletions

File tree

invokeai/frontend/web/public/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,10 @@
920920
"unknownTemplate": "Unknown Template",
921921
"unkownInvocation": "Unknown Invocation type",
922922
"updateNode": "Update Node",
923+
"updateAllNodes": "Update All Nodes",
923924
"updateApp": "Update App",
925+
"unableToUpdateNodes_one": "Unable to update {{count}} node",
926+
"unableToUpdateNodes_other": "Unable to update {{count}} nodes",
924927
"vaeField": "Vae",
925928
"vaeFieldDescription": "Vae submodel.",
926929
"vaeModelField": "VAE",

invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa
7272
import { addTabChangedListener } from './listeners/tabChanged';
7373
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
7474
import { addWorkflowLoadedListener } from './listeners/workflowLoaded';
75+
import { addUpdateAllNodesRequestedListener } from './listeners/updateAllNodesRequested';
7576

7677
export const listenerMiddleware = createListenerMiddleware();
7778

@@ -178,6 +179,7 @@ addReceivedOpenAPISchemaListener();
178179

179180
// Workflows
180181
addWorkflowLoadedListener();
182+
addUpdateAllNodesRequestedListener();
181183

182184
// DND
183185
addImageDroppedListener();
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {
2+
getNeedsUpdate,
3+
updateNode,
4+
} from 'features/nodes/hooks/useNodeVersion';
5+
import { updateAllNodesRequested } from 'features/nodes/store/actions';
6+
import { nodeReplaced } from 'features/nodes/store/nodesSlice';
7+
import { startAppListening } from '..';
8+
import { logger } from 'app/logging/logger';
9+
import { addToast } from 'features/system/store/systemSlice';
10+
import { makeToast } from 'features/system/util/makeToast';
11+
import { t } from 'i18next';
12+
13+
export const addUpdateAllNodesRequestedListener = () => {
14+
startAppListening({
15+
actionCreator: updateAllNodesRequested,
16+
effect: (action, { dispatch, getState }) => {
17+
const log = logger('nodes');
18+
const nodes = getState().nodes.nodes;
19+
const templates = getState().nodes.nodeTemplates;
20+
21+
let unableToUpdateCount = 0;
22+
23+
nodes.forEach((node) => {
24+
const template = templates[node.data.type];
25+
const needsUpdate = getNeedsUpdate(node, template);
26+
const updatedNode = updateNode(node, template);
27+
if (!updatedNode) {
28+
if (needsUpdate) {
29+
unableToUpdateCount++;
30+
}
31+
return;
32+
}
33+
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
34+
});
35+
36+
if (unableToUpdateCount) {
37+
log.warn(
38+
`Unable to update ${unableToUpdateCount} nodes. Please report this issue.`
39+
);
40+
dispatch(
41+
addToast(
42+
makeToast({
43+
title: t('nodes.unableToUpdateNodes', {
44+
count: unableToUpdateCount,
45+
}),
46+
})
47+
)
48+
);
49+
}
50+
},
51+
});
52+
};

invokeai/frontend/web/src/features/nodes/components/flow/panels/TopLeftPanel/TopLeftPanel.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@ import { useAppDispatch } from 'app/store/storeHooks';
33
import IAIIconButton from 'common/components/IAIIconButton';
44
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
55
import { memo, useCallback } from 'react';
6-
import { FaPlus } from 'react-icons/fa';
6+
import { FaPlus, FaSync } from 'react-icons/fa';
77
import { useTranslation } from 'react-i18next';
8+
import IAIButton from 'common/components/IAIButton';
9+
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
10+
import { updateAllNodesRequested } from 'features/nodes/store/actions';
811

912
const TopLeftPanel = () => {
1013
const dispatch = useAppDispatch();
1114
const { t } = useTranslation();
15+
const nodesNeedUpdate = useGetNodesNeedUpdate();
1216
const handleOpenAddNodePopover = useCallback(() => {
1317
dispatch(addNodePopoverOpened());
1418
}, [dispatch]);
19+
const handleClickUpdateNodes = useCallback(() => {
20+
dispatch(updateAllNodesRequested());
21+
}, [dispatch]);
1522

1623
return (
1724
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
@@ -21,6 +28,11 @@ const TopLeftPanel = () => {
2128
icon={<FaPlus />}
2229
onClick={handleOpenAddNodePopover}
2330
/>
31+
{nodesNeedUpdate && (
32+
<IAIButton leftIcon={<FaSync />} onClick={handleClickUpdateNodes}>
33+
{t('nodes.updateAllNodes')}
34+
</IAIButton>
35+
)}
2436
</Flex>
2537
);
2638
};

invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ const Content = (props: {
107107
{props.node.data.version}
108108
</Text>
109109
</FormControl>
110-
{mayUpdate && (
110+
{needsUpdate && (
111111
<IAIIconButton
112112
aria-label={t('nodes.updateNode')}
113113
tooltip={t('nodes.updateNode')}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { createSelector } from '@reduxjs/toolkit';
2+
import { stateSelector } from 'app/store/store';
3+
import { useAppSelector } from 'app/store/storeHooks';
4+
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
5+
import { getNeedsUpdate } from './useNodeVersion';
6+
7+
const selector = createSelector(
8+
stateSelector,
9+
(state) => {
10+
const nodes = state.nodes.nodes;
11+
const templates = state.nodes.nodeTemplates;
12+
13+
const needsUpdate = nodes.some((node) => {
14+
const template = templates[node.data.type];
15+
return getNeedsUpdate(node, template);
16+
});
17+
return needsUpdate;
18+
},
19+
defaultSelectorOptions
20+
);
21+
22+
export const useGetNodesNeedUpdate = () => {
23+
const getNeedsUpdate = useAppSelector(selector);
24+
return getNeedsUpdate;
25+
};

invokeai/frontend/web/src/features/nodes/hooks/useNodeVersion.ts

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,80 @@ import { stateSelector } from 'app/store/store';
33
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
44
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
55
import { satisfies } from 'compare-versions';
6+
import { cloneDeep, defaultsDeep } from 'lodash-es';
67
import { useCallback, useMemo } from 'react';
8+
import { Node } from 'reactflow';
9+
import { AnyInvocationType } from 'services/events/types';
10+
import { nodeReplaced } from '../store/nodesSlice';
11+
import { buildNodeData } from '../store/util/buildNodeData';
712
import {
813
InvocationNodeData,
14+
InvocationTemplate,
15+
NodeData,
916
isInvocationNode,
1017
zParsedSemver,
1118
} from '../types/types';
12-
import { cloneDeep, defaultsDeep } from 'lodash-es';
13-
import { buildNodeData } from '../store/util/buildNodeData';
14-
import { AnyInvocationType } from 'services/events/types';
15-
import { Node } from 'reactflow';
16-
import { nodeReplaced } from '../store/nodesSlice';
19+
import { useAppToaster } from 'app/components/Toaster';
20+
import { useTranslation } from 'react-i18next';
21+
22+
export const getNeedsUpdate = (
23+
node?: Node<NodeData>,
24+
template?: InvocationTemplate
25+
) => {
26+
if (!isInvocationNode(node) || !template) {
27+
return false;
28+
}
29+
return node.data.version !== template.version;
30+
};
31+
32+
export const getMayUpdateNode = (
33+
node?: Node<NodeData>,
34+
template?: InvocationTemplate
35+
) => {
36+
const needsUpdate = getNeedsUpdate(node, template);
37+
if (
38+
!needsUpdate ||
39+
!isInvocationNode(node) ||
40+
!template ||
41+
!node.data.version
42+
) {
43+
return false;
44+
}
45+
const templateMajor = zParsedSemver.parse(template.version).major;
46+
47+
return satisfies(node.data.version, `^${templateMajor}`);
48+
};
49+
50+
export const updateNode = (
51+
node?: Node<NodeData>,
52+
template?: InvocationTemplate
53+
) => {
54+
const mayUpdate = getMayUpdateNode(node, template);
55+
if (
56+
!mayUpdate ||
57+
!isInvocationNode(node) ||
58+
!template ||
59+
!node.data.version
60+
) {
61+
return;
62+
}
63+
64+
const defaults = buildNodeData(
65+
node.data.type as AnyInvocationType,
66+
node.position,
67+
template
68+
) as Node<InvocationNodeData>;
69+
70+
const clone = cloneDeep(node);
71+
clone.data.version = template.version;
72+
defaultsDeep(clone, defaults);
73+
return clone;
74+
};
1775

1876
export const useNodeVersion = (nodeId: string) => {
1977
const dispatch = useAppDispatch();
78+
const toast = useAppToaster();
79+
const { t } = useTranslation();
2080
const selector = useMemo(
2181
() =>
2282
createSelector(
@@ -33,48 +93,27 @@ export const useNodeVersion = (nodeId: string) => {
3393

3494
const { node, nodeTemplate } = useAppSelector(selector);
3595

36-
const needsUpdate = useMemo(() => {
37-
if (!isInvocationNode(node) || !nodeTemplate) {
38-
return false;
39-
}
40-
return node.data.version !== nodeTemplate.version;
41-
}, [node, nodeTemplate]);
42-
43-
const mayUpdate = useMemo(() => {
44-
if (
45-
!needsUpdate ||
46-
!isInvocationNode(node) ||
47-
!nodeTemplate ||
48-
!node.data.version
49-
) {
50-
return false;
51-
}
52-
const templateMajor = zParsedSemver.parse(nodeTemplate.version).major;
96+
const needsUpdate = useMemo(
97+
() => getNeedsUpdate(node, nodeTemplate),
98+
[node, nodeTemplate]
99+
);
53100

54-
return satisfies(node.data.version, `^${templateMajor}`);
55-
}, [needsUpdate, node, nodeTemplate]);
101+
const mayUpdate = useMemo(
102+
() => getMayUpdateNode(node, nodeTemplate),
103+
[node, nodeTemplate]
104+
);
56105

57-
const updateNode = useCallback(() => {
58-
if (
59-
!mayUpdate ||
60-
!isInvocationNode(node) ||
61-
!nodeTemplate ||
62-
!node.data.version
63-
) {
106+
const _updateNode = useCallback(() => {
107+
const needsUpdate = getNeedsUpdate(node, nodeTemplate);
108+
const updatedNode = updateNode(node, nodeTemplate);
109+
if (!updatedNode) {
110+
if (needsUpdate) {
111+
toast({ title: t('nodes.unableToUpdateNodes', { count: 1 }) });
112+
}
64113
return;
65114
}
115+
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
116+
}, [dispatch, node, nodeTemplate, t, toast]);
66117

67-
const defaults = buildNodeData(
68-
node.data.type as AnyInvocationType,
69-
node.position,
70-
nodeTemplate
71-
) as Node<InvocationNodeData>;
72-
73-
const clone = cloneDeep(node);
74-
clone.data.version = nodeTemplate.version;
75-
defaultsDeep(clone, defaults);
76-
dispatch(nodeReplaced({ nodeId: clone.id, node: clone }));
77-
}, [dispatch, mayUpdate, node, nodeTemplate]);
78-
79-
return { needsUpdate, mayUpdate, updateNode };
118+
return { needsUpdate, mayUpdate, updateNode: _updateNode };
80119
};

invokeai/frontend/web/src/features/nodes/store/actions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ export const isAnyGraphBuilt = isAnyOf(
2121
export const workflowLoadRequested = createAction<Workflow>(
2222
'nodes/workflowLoadRequested'
2323
);
24+
25+
export const updateAllNodesRequested = createAction(
26+
'nodes/updateAllNodesRequested'
27+
);

0 commit comments

Comments
 (0)