mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add Update All Nodes button
This commit is contained in:
parent
3f6e8e9d6b
commit
7fcf475aec
@ -920,7 +920,10 @@
|
||||
"unknownTemplate": "Unknown Template",
|
||||
"unkownInvocation": "Unknown Invocation type",
|
||||
"updateNode": "Update Node",
|
||||
"updateAllNodes": "Update All Nodes",
|
||||
"updateApp": "Update App",
|
||||
"unableToUpdateNodes_one": "Unable to update {{count}} node",
|
||||
"unableToUpdateNodes_other": "Unable to update {{count}} nodes",
|
||||
"vaeField": "Vae",
|
||||
"vaeFieldDescription": "Vae submodel.",
|
||||
"vaeModelField": "VAE",
|
||||
|
@ -72,6 +72,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa
|
||||
import { addTabChangedListener } from './listeners/tabChanged';
|
||||
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
|
||||
import { addWorkflowLoadedListener } from './listeners/workflowLoaded';
|
||||
import { addUpdateAllNodesRequestedListener } from './listeners/updateAllNodesRequested';
|
||||
|
||||
export const listenerMiddleware = createListenerMiddleware();
|
||||
|
||||
@ -178,6 +179,7 @@ addReceivedOpenAPISchemaListener();
|
||||
|
||||
// Workflows
|
||||
addWorkflowLoadedListener();
|
||||
addUpdateAllNodesRequestedListener();
|
||||
|
||||
// DND
|
||||
addImageDroppedListener();
|
||||
|
@ -0,0 +1,52 @@
|
||||
import {
|
||||
getNeedsUpdate,
|
||||
updateNode,
|
||||
} from 'features/nodes/hooks/useNodeVersion';
|
||||
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
||||
import { nodeReplaced } from 'features/nodes/store/nodesSlice';
|
||||
import { startAppListening } from '..';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
import { makeToast } from 'features/system/util/makeToast';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const addUpdateAllNodesRequestedListener = () => {
|
||||
startAppListening({
|
||||
actionCreator: updateAllNodesRequested,
|
||||
effect: (action, { dispatch, getState }) => {
|
||||
const log = logger('nodes');
|
||||
const nodes = getState().nodes.nodes;
|
||||
const templates = getState().nodes.nodeTemplates;
|
||||
|
||||
let unableToUpdateCount = 0;
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const template = templates[node.data.type];
|
||||
const needsUpdate = getNeedsUpdate(node, template);
|
||||
const updatedNode = updateNode(node, template);
|
||||
if (!updatedNode) {
|
||||
if (needsUpdate) {
|
||||
unableToUpdateCount++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
|
||||
});
|
||||
|
||||
if (unableToUpdateCount) {
|
||||
log.warn(
|
||||
`Unable to update ${unableToUpdateCount} nodes. Please report this issue.`
|
||||
);
|
||||
dispatch(
|
||||
addToast(
|
||||
makeToast({
|
||||
title: t('nodes.unableToUpdateNodes', {
|
||||
count: unableToUpdateCount,
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -3,15 +3,22 @@ import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { FaPlus, FaSync } from 'react-icons/fa';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate';
|
||||
import { updateAllNodesRequested } from 'features/nodes/store/actions';
|
||||
|
||||
const TopLeftPanel = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const nodesNeedUpdate = useGetNodesNeedUpdate();
|
||||
const handleOpenAddNodePopover = useCallback(() => {
|
||||
dispatch(addNodePopoverOpened());
|
||||
}, [dispatch]);
|
||||
const handleClickUpdateNodes = useCallback(() => {
|
||||
dispatch(updateAllNodesRequested());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
|
||||
@ -21,6 +28,11 @@ const TopLeftPanel = () => {
|
||||
icon={<FaPlus />}
|
||||
onClick={handleOpenAddNodePopover}
|
||||
/>
|
||||
{nodesNeedUpdate && (
|
||||
<IAIButton leftIcon={<FaSync />} onClick={handleClickUpdateNodes}>
|
||||
{t('nodes.updateAllNodes')}
|
||||
</IAIButton>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -107,7 +107,7 @@ const Content = (props: {
|
||||
{props.node.data.version}
|
||||
</Text>
|
||||
</FormControl>
|
||||
{mayUpdate && (
|
||||
{needsUpdate && (
|
||||
<IAIIconButton
|
||||
aria-label={t('nodes.updateNode')}
|
||||
tooltip={t('nodes.updateNode')}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { stateSelector } from 'app/store/store';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { getNeedsUpdate } from './useNodeVersion';
|
||||
|
||||
const selector = createSelector(
|
||||
stateSelector,
|
||||
(state) => {
|
||||
const nodes = state.nodes.nodes;
|
||||
const templates = state.nodes.nodeTemplates;
|
||||
|
||||
const needsUpdate = nodes.some((node) => {
|
||||
const template = templates[node.data.type];
|
||||
return getNeedsUpdate(node, template);
|
||||
});
|
||||
return needsUpdate;
|
||||
},
|
||||
defaultSelectorOptions
|
||||
);
|
||||
|
||||
export const useGetNodesNeedUpdate = () => {
|
||||
const getNeedsUpdate = useAppSelector(selector);
|
||||
return getNeedsUpdate;
|
||||
};
|
@ -3,20 +3,80 @@ import { stateSelector } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||
import { satisfies } from 'compare-versions';
|
||||
import { cloneDeep, defaultsDeep } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Node } from 'reactflow';
|
||||
import { AnyInvocationType } from 'services/events/types';
|
||||
import { nodeReplaced } from '../store/nodesSlice';
|
||||
import { buildNodeData } from '../store/util/buildNodeData';
|
||||
import {
|
||||
InvocationNodeData,
|
||||
InvocationTemplate,
|
||||
NodeData,
|
||||
isInvocationNode,
|
||||
zParsedSemver,
|
||||
} from '../types/types';
|
||||
import { cloneDeep, defaultsDeep } from 'lodash-es';
|
||||
import { buildNodeData } from '../store/util/buildNodeData';
|
||||
import { AnyInvocationType } from 'services/events/types';
|
||||
import { Node } from 'reactflow';
|
||||
import { nodeReplaced } from '../store/nodesSlice';
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const getNeedsUpdate = (
|
||||
node?: Node<NodeData>,
|
||||
template?: InvocationTemplate
|
||||
) => {
|
||||
if (!isInvocationNode(node) || !template) {
|
||||
return false;
|
||||
}
|
||||
return node.data.version !== template.version;
|
||||
};
|
||||
|
||||
export const getMayUpdateNode = (
|
||||
node?: Node<NodeData>,
|
||||
template?: InvocationTemplate
|
||||
) => {
|
||||
const needsUpdate = getNeedsUpdate(node, template);
|
||||
if (
|
||||
!needsUpdate ||
|
||||
!isInvocationNode(node) ||
|
||||
!template ||
|
||||
!node.data.version
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const templateMajor = zParsedSemver.parse(template.version).major;
|
||||
|
||||
return satisfies(node.data.version, `^${templateMajor}`);
|
||||
};
|
||||
|
||||
export const updateNode = (
|
||||
node?: Node<NodeData>,
|
||||
template?: InvocationTemplate
|
||||
) => {
|
||||
const mayUpdate = getMayUpdateNode(node, template);
|
||||
if (
|
||||
!mayUpdate ||
|
||||
!isInvocationNode(node) ||
|
||||
!template ||
|
||||
!node.data.version
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaults = buildNodeData(
|
||||
node.data.type as AnyInvocationType,
|
||||
node.position,
|
||||
template
|
||||
) as Node<InvocationNodeData>;
|
||||
|
||||
const clone = cloneDeep(node);
|
||||
clone.data.version = template.version;
|
||||
defaultsDeep(clone, defaults);
|
||||
return clone;
|
||||
};
|
||||
|
||||
export const useNodeVersion = (nodeId: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useAppToaster();
|
||||
const { t } = useTranslation();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
@ -33,48 +93,27 @@ export const useNodeVersion = (nodeId: string) => {
|
||||
|
||||
const { node, nodeTemplate } = useAppSelector(selector);
|
||||
|
||||
const needsUpdate = useMemo(() => {
|
||||
if (!isInvocationNode(node) || !nodeTemplate) {
|
||||
return false;
|
||||
}
|
||||
return node.data.version !== nodeTemplate.version;
|
||||
}, [node, nodeTemplate]);
|
||||
const needsUpdate = useMemo(
|
||||
() => getNeedsUpdate(node, nodeTemplate),
|
||||
[node, nodeTemplate]
|
||||
);
|
||||
|
||||
const mayUpdate = useMemo(() => {
|
||||
if (
|
||||
!needsUpdate ||
|
||||
!isInvocationNode(node) ||
|
||||
!nodeTemplate ||
|
||||
!node.data.version
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const templateMajor = zParsedSemver.parse(nodeTemplate.version).major;
|
||||
const mayUpdate = useMemo(
|
||||
() => getMayUpdateNode(node, nodeTemplate),
|
||||
[node, nodeTemplate]
|
||||
);
|
||||
|
||||
return satisfies(node.data.version, `^${templateMajor}`);
|
||||
}, [needsUpdate, node, nodeTemplate]);
|
||||
|
||||
const updateNode = useCallback(() => {
|
||||
if (
|
||||
!mayUpdate ||
|
||||
!isInvocationNode(node) ||
|
||||
!nodeTemplate ||
|
||||
!node.data.version
|
||||
) {
|
||||
const _updateNode = useCallback(() => {
|
||||
const needsUpdate = getNeedsUpdate(node, nodeTemplate);
|
||||
const updatedNode = updateNode(node, nodeTemplate);
|
||||
if (!updatedNode) {
|
||||
if (needsUpdate) {
|
||||
toast({ title: t('nodes.unableToUpdateNodes', { count: 1 }) });
|
||||
}
|
||||
return;
|
||||
}
|
||||
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
|
||||
}, [dispatch, node, nodeTemplate, t, toast]);
|
||||
|
||||
const defaults = buildNodeData(
|
||||
node.data.type as AnyInvocationType,
|
||||
node.position,
|
||||
nodeTemplate
|
||||
) as Node<InvocationNodeData>;
|
||||
|
||||
const clone = cloneDeep(node);
|
||||
clone.data.version = nodeTemplate.version;
|
||||
defaultsDeep(clone, defaults);
|
||||
dispatch(nodeReplaced({ nodeId: clone.id, node: clone }));
|
||||
}, [dispatch, mayUpdate, node, nodeTemplate]);
|
||||
|
||||
return { needsUpdate, mayUpdate, updateNode };
|
||||
return { needsUpdate, mayUpdate, updateNode: _updateNode };
|
||||
};
|
||||
|
@ -21,3 +21,7 @@ export const isAnyGraphBuilt = isAnyOf(
|
||||
export const workflowLoadRequested = createAction<Workflow>(
|
||||
'nodes/workflowLoadRequested'
|
||||
);
|
||||
|
||||
export const updateAllNodesRequested = createAction(
|
||||
'nodes/updateAllNodesRequested'
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user