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",
|
"unknownTemplate": "Unknown Template",
|
||||||
"unkownInvocation": "Unknown Invocation type",
|
"unkownInvocation": "Unknown Invocation type",
|
||||||
"updateNode": "Update Node",
|
"updateNode": "Update Node",
|
||||||
|
"updateAllNodes": "Update All Nodes",
|
||||||
"updateApp": "Update App",
|
"updateApp": "Update App",
|
||||||
|
"unableToUpdateNodes_one": "Unable to update {{count}} node",
|
||||||
|
"unableToUpdateNodes_other": "Unable to update {{count}} nodes",
|
||||||
"vaeField": "Vae",
|
"vaeField": "Vae",
|
||||||
"vaeFieldDescription": "Vae submodel.",
|
"vaeFieldDescription": "Vae submodel.",
|
||||||
"vaeModelField": "VAE",
|
"vaeModelField": "VAE",
|
||||||
|
@ -72,6 +72,7 @@ import { addStagingAreaImageSavedListener } from './listeners/stagingAreaImageSa
|
|||||||
import { addTabChangedListener } from './listeners/tabChanged';
|
import { addTabChangedListener } from './listeners/tabChanged';
|
||||||
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
|
import { addUpscaleRequestedListener } from './listeners/upscaleRequested';
|
||||||
import { addWorkflowLoadedListener } from './listeners/workflowLoaded';
|
import { addWorkflowLoadedListener } from './listeners/workflowLoaded';
|
||||||
|
import { addUpdateAllNodesRequestedListener } from './listeners/updateAllNodesRequested';
|
||||||
|
|
||||||
export const listenerMiddleware = createListenerMiddleware();
|
export const listenerMiddleware = createListenerMiddleware();
|
||||||
|
|
||||||
@ -178,6 +179,7 @@ addReceivedOpenAPISchemaListener();
|
|||||||
|
|
||||||
// Workflows
|
// Workflows
|
||||||
addWorkflowLoadedListener();
|
addWorkflowLoadedListener();
|
||||||
|
addUpdateAllNodesRequestedListener();
|
||||||
|
|
||||||
// DND
|
// DND
|
||||||
addImageDroppedListener();
|
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 IAIIconButton from 'common/components/IAIIconButton';
|
||||||
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
import { addNodePopoverOpened } from 'features/nodes/store/nodesSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { FaPlus } from 'react-icons/fa';
|
import { FaPlus, FaSync } from 'react-icons/fa';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 TopLeftPanel = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const nodesNeedUpdate = useGetNodesNeedUpdate();
|
||||||
const handleOpenAddNodePopover = useCallback(() => {
|
const handleOpenAddNodePopover = useCallback(() => {
|
||||||
dispatch(addNodePopoverOpened());
|
dispatch(addNodePopoverOpened());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
const handleClickUpdateNodes = useCallback(() => {
|
||||||
|
dispatch(updateAllNodesRequested());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
|
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineStart: 2 }}>
|
||||||
@ -21,6 +28,11 @@ const TopLeftPanel = () => {
|
|||||||
icon={<FaPlus />}
|
icon={<FaPlus />}
|
||||||
onClick={handleOpenAddNodePopover}
|
onClick={handleOpenAddNodePopover}
|
||||||
/>
|
/>
|
||||||
|
{nodesNeedUpdate && (
|
||||||
|
<IAIButton leftIcon={<FaSync />} onClick={handleClickUpdateNodes}>
|
||||||
|
{t('nodes.updateAllNodes')}
|
||||||
|
</IAIButton>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -107,7 +107,7 @@ const Content = (props: {
|
|||||||
{props.node.data.version}
|
{props.node.data.version}
|
||||||
</Text>
|
</Text>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{mayUpdate && (
|
{needsUpdate && (
|
||||||
<IAIIconButton
|
<IAIIconButton
|
||||||
aria-label={t('nodes.updateNode')}
|
aria-label={t('nodes.updateNode')}
|
||||||
tooltip={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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
|
||||||
import { satisfies } from 'compare-versions';
|
import { satisfies } from 'compare-versions';
|
||||||
|
import { cloneDeep, defaultsDeep } from 'lodash-es';
|
||||||
import { useCallback, useMemo } from 'react';
|
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 {
|
import {
|
||||||
InvocationNodeData,
|
InvocationNodeData,
|
||||||
|
InvocationTemplate,
|
||||||
|
NodeData,
|
||||||
isInvocationNode,
|
isInvocationNode,
|
||||||
zParsedSemver,
|
zParsedSemver,
|
||||||
} from '../types/types';
|
} from '../types/types';
|
||||||
import { cloneDeep, defaultsDeep } from 'lodash-es';
|
import { useAppToaster } from 'app/components/Toaster';
|
||||||
import { buildNodeData } from '../store/util/buildNodeData';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AnyInvocationType } from 'services/events/types';
|
|
||||||
import { Node } from 'reactflow';
|
export const getNeedsUpdate = (
|
||||||
import { nodeReplaced } from '../store/nodesSlice';
|
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) => {
|
export const useNodeVersion = (nodeId: string) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const toast = useAppToaster();
|
||||||
|
const { t } = useTranslation();
|
||||||
const selector = useMemo(
|
const selector = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createSelector(
|
createSelector(
|
||||||
@ -33,48 +93,27 @@ export const useNodeVersion = (nodeId: string) => {
|
|||||||
|
|
||||||
const { node, nodeTemplate } = useAppSelector(selector);
|
const { node, nodeTemplate } = useAppSelector(selector);
|
||||||
|
|
||||||
const needsUpdate = useMemo(() => {
|
const needsUpdate = useMemo(
|
||||||
if (!isInvocationNode(node) || !nodeTemplate) {
|
() => getNeedsUpdate(node, nodeTemplate),
|
||||||
return false;
|
[node, nodeTemplate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const mayUpdate = useMemo(
|
||||||
|
() => getMayUpdateNode(node, nodeTemplate),
|
||||||
|
[node, nodeTemplate]
|
||||||
|
);
|
||||||
|
|
||||||
|
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 node.data.version !== nodeTemplate.version;
|
|
||||||
}, [node, nodeTemplate]);
|
|
||||||
|
|
||||||
const mayUpdate = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!needsUpdate ||
|
|
||||||
!isInvocationNode(node) ||
|
|
||||||
!nodeTemplate ||
|
|
||||||
!node.data.version
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const templateMajor = zParsedSemver.parse(nodeTemplate.version).major;
|
|
||||||
|
|
||||||
return satisfies(node.data.version, `^${templateMajor}`);
|
|
||||||
}, [needsUpdate, node, nodeTemplate]);
|
|
||||||
|
|
||||||
const updateNode = useCallback(() => {
|
|
||||||
if (
|
|
||||||
!mayUpdate ||
|
|
||||||
!isInvocationNode(node) ||
|
|
||||||
!nodeTemplate ||
|
|
||||||
!node.data.version
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
dispatch(nodeReplaced({ nodeId: updatedNode.id, node: updatedNode }));
|
||||||
|
}, [dispatch, node, nodeTemplate, t, toast]);
|
||||||
|
|
||||||
const defaults = buildNodeData(
|
return { needsUpdate, mayUpdate, updateNode: _updateNode };
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
@ -21,3 +21,7 @@ export const isAnyGraphBuilt = isAnyOf(
|
|||||||
export const workflowLoadRequested = createAction<Workflow>(
|
export const workflowLoadRequested = createAction<Workflow>(
|
||||||
'nodes/workflowLoadRequested'
|
'nodes/workflowLoadRequested'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const updateAllNodesRequested = createAction(
|
||||||
|
'nodes/updateAllNodesRequested'
|
||||||
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user