mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-02-13 10:54:58 -05:00
perf(ui): optimize redux selectors for workflow editor
- Build selectors for each node in a react context so components can re-use the same selectors - Cache the selectors in the context
This commit is contained in:
@@ -15,6 +15,8 @@ import { useDndMonitor } from 'features/dnd/useDndMonitor';
|
||||
import { useDynamicPromptsWatcher } from 'features/dynamicPrompts/hooks/useDynamicPromptsWatcher';
|
||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||
import { useWorkflowBuilderWatcher } from 'features/nodes/components/sidePanel/workflow/IsolatedWorkflowBuilderWatcher';
|
||||
import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import { useSyncNodeErrors } from 'features/nodes/store/util/fieldValidators';
|
||||
import { useReadinessWatcher } from 'features/queue/store/readiness';
|
||||
import { configChanged } from 'features/system/store/configSlice';
|
||||
import { selectLanguage } from 'features/system/store/systemSelectors';
|
||||
@@ -47,10 +49,12 @@ export const GlobalHookIsolator = memo(
|
||||
useCloseChakraTooltipsOnDragFix();
|
||||
useNavigationApi();
|
||||
useDndMonitor();
|
||||
useSyncNodeErrors();
|
||||
|
||||
// Persistent subscription to the queue counts query - canvas relies on this to know if there are pending
|
||||
// and/or in progress canvas sessions.
|
||||
useGetQueueCountsByDestinationQuery(queueCountArg);
|
||||
useSyncExecutionState();
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language);
|
||||
|
||||
@@ -18,3 +18,5 @@ export const getSelectorsOptions = {
|
||||
argsMemoize: lruMemoize,
|
||||
}),
|
||||
};
|
||||
|
||||
export const createLruSelector = createSelectorCreator(lruMemoize);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import {
|
||||
@@ -14,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { useAddImagesToBoardMutation, useRemoveImagesFromBoardMutation } from 'services/api/endpoints/images';
|
||||
|
||||
const selectImagesToChange = createMemoizedSelector(
|
||||
const selectImagesToChange = createSelector(
|
||||
selectChangeBoardModalSlice,
|
||||
(changeBoardModal) => changeBoardModal.image_names
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ import { useConnection } from 'features/nodes/hooks/useConnection';
|
||||
import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection';
|
||||
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
|
||||
import { useNodeCopyPaste } from 'features/nodes/hooks/useNodeCopyPaste';
|
||||
import { useSyncExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
import {
|
||||
$addNodeCmdk,
|
||||
$cursorPos,
|
||||
@@ -83,23 +82,16 @@ export const Flow = memo(() => {
|
||||
const nodes = useAppSelector(selectNodes);
|
||||
const edges = useAppSelector(selectEdges);
|
||||
const viewport = useStore($viewport);
|
||||
const needsFit = useStore($needsFit);
|
||||
const mayUndo = useAppSelector(selectMayUndo);
|
||||
const mayRedo = useAppSelector(selectMayRedo);
|
||||
const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid);
|
||||
const selectionMode = useAppSelector(selectSelectionMode);
|
||||
const { onConnectStart, onConnect, onConnectEnd } = useConnection();
|
||||
const flowWrapper = useRef<HTMLDivElement>(null);
|
||||
const isValidConnection = useIsValidConnection();
|
||||
const cancelConnection = useReactFlowStore(selectCancelConnection);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const store = useAppStore();
|
||||
const isWorkflowsFocused = useIsRegionFocused('workflows');
|
||||
const isLocked = useIsWorkflowEditorLocked();
|
||||
|
||||
useFocusRegion('workflows', flowWrapper);
|
||||
|
||||
useSyncExecutionState();
|
||||
const [borderRadius] = useToken('radii', ['base']);
|
||||
const flowStyles = useMemo<CSSProperties>(() => ({ borderRadius }), [borderRadius]);
|
||||
|
||||
@@ -110,12 +102,12 @@ export const Flow = memo(() => {
|
||||
if (!flow) {
|
||||
return;
|
||||
}
|
||||
if (needsFit) {
|
||||
if ($needsFit.get()) {
|
||||
$needsFit.set(false);
|
||||
flow.fitView();
|
||||
}
|
||||
},
|
||||
[dispatch, needsFit]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onEdgesChange: OnEdgesChange<AnyEdge> = useCallback(
|
||||
@@ -214,6 +206,83 @@ export const Flow = memo(() => {
|
||||
|
||||
// #endregion
|
||||
|
||||
const onNodeClick = useCallback<NodeMouseHandler<AnyNode>>((e, node) => {
|
||||
if (!$isSelectingOutputNode.get()) {
|
||||
return;
|
||||
}
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
const { id } = node.data;
|
||||
$outputNodeId.set(id);
|
||||
$isSelectingOutputNode.set(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactFlow<AnyNode, AnyEdge>
|
||||
id="workflow-editor"
|
||||
ref={flowWrapper}
|
||||
defaultViewport={viewport}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onNodeClick={onNodeClick}
|
||||
onMouseMove={onMouseMove}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onReconnect={onReconnect}
|
||||
onReconnectStart={onReconnectStart}
|
||||
onReconnectEnd={onReconnectEnd}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnect={onConnect}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onMoveEnd={handleMoveEnd}
|
||||
connectionLineComponent={CustomConnectionLine}
|
||||
isValidConnection={isValidConnection}
|
||||
edgesFocusable={!isLocked}
|
||||
edgesReconnectable={!isLocked}
|
||||
nodesDraggable={!isLocked}
|
||||
nodesConnectable={!isLocked}
|
||||
nodesFocusable={!isLocked}
|
||||
elementsSelectable={!isLocked}
|
||||
minZoom={0.1}
|
||||
snapToGrid={shouldSnapToGrid}
|
||||
snapGrid={snapGrid}
|
||||
connectionRadius={30}
|
||||
proOptions={proOptions}
|
||||
style={flowStyles}
|
||||
onPaneClick={handlePaneClick}
|
||||
deleteKeyCode={null}
|
||||
selectionMode={selectionMode}
|
||||
elevateEdgesOnSelect
|
||||
nodeDragThreshold={1}
|
||||
noDragClassName={NO_DRAG_CLASS}
|
||||
noWheelClassName={NO_WHEEL_CLASS}
|
||||
noPanClassName={NO_PAN_CLASS}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
<HotkeyIsolator />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Flow.displayName = 'Flow';
|
||||
|
||||
const HotkeyIsolator = memo(() => {
|
||||
const isLocked = useIsWorkflowEditorLocked();
|
||||
|
||||
const mayUndo = useAppSelector(selectMayUndo);
|
||||
const mayRedo = useAppSelector(selectMayRedo);
|
||||
|
||||
const cancelConnection = useReactFlowStore(selectCancelConnection);
|
||||
|
||||
const store = useAppStore();
|
||||
const isWorkflowsFocused = useIsRegionFocused('workflows');
|
||||
|
||||
const { copySelection, pasteSelection, pasteSelectionWithEdges } = useNodeCopyPaste();
|
||||
|
||||
useRegisteredHotkeys({
|
||||
@@ -239,12 +308,12 @@ export const Flow = memo(() => {
|
||||
}
|
||||
});
|
||||
if (nodeChanges.length > 0) {
|
||||
dispatch(nodesChanged(nodeChanges));
|
||||
store.dispatch(nodesChanged(nodeChanges));
|
||||
}
|
||||
if (edgeChanges.length > 0) {
|
||||
dispatch(edgesChanged(edgeChanges));
|
||||
store.dispatch(edgesChanged(edgeChanges));
|
||||
}
|
||||
}, [dispatch, store]);
|
||||
}, [store]);
|
||||
useRegisteredHotkeys({
|
||||
id: 'selectAll',
|
||||
category: 'workflows',
|
||||
@@ -273,20 +342,20 @@ export const Flow = memo(() => {
|
||||
id: 'undo',
|
||||
category: 'workflows',
|
||||
callback: () => {
|
||||
dispatch(undo());
|
||||
store.dispatch(undo());
|
||||
},
|
||||
options: { enabled: isWorkflowsFocused && !isLocked && mayUndo, preventDefault: true },
|
||||
dependencies: [mayUndo, isLocked, isWorkflowsFocused],
|
||||
dependencies: [store, mayUndo, isLocked, isWorkflowsFocused],
|
||||
});
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'redo',
|
||||
category: 'workflows',
|
||||
callback: () => {
|
||||
dispatch(redo());
|
||||
store.dispatch(redo());
|
||||
},
|
||||
options: { enabled: isWorkflowsFocused && !isLocked && mayRedo, preventDefault: true },
|
||||
dependencies: [mayRedo, isLocked, isWorkflowsFocused],
|
||||
dependencies: [store, mayRedo, isLocked, isWorkflowsFocused],
|
||||
});
|
||||
|
||||
const onEscapeHotkey = useCallback(() => {
|
||||
@@ -313,12 +382,12 @@ export const Flow = memo(() => {
|
||||
edgeChanges.push({ type: 'remove', id });
|
||||
});
|
||||
if (nodeChanges.length > 0) {
|
||||
dispatch(nodesChanged(nodeChanges));
|
||||
store.dispatch(nodesChanged(nodeChanges));
|
||||
}
|
||||
if (edgeChanges.length > 0) {
|
||||
dispatch(edgesChanged(edgeChanges));
|
||||
store.dispatch(edgesChanged(edgeChanges));
|
||||
}
|
||||
}, [dispatch, store]);
|
||||
}, [store]);
|
||||
useRegisteredHotkeys({
|
||||
id: 'deleteSelection',
|
||||
category: 'workflows',
|
||||
@@ -327,65 +396,6 @@ export const Flow = memo(() => {
|
||||
dependencies: [deleteSelection, isWorkflowsFocused, isLocked],
|
||||
});
|
||||
|
||||
const onNodeClick = useCallback<NodeMouseHandler<AnyNode>>((e, node) => {
|
||||
if (!$isSelectingOutputNode.get()) {
|
||||
return;
|
||||
}
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
}
|
||||
const { id } = node.data;
|
||||
$outputNodeId.set(id);
|
||||
$isSelectingOutputNode.set(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ReactFlow<AnyNode, AnyEdge>
|
||||
id="workflow-editor"
|
||||
ref={flowWrapper}
|
||||
defaultViewport={viewport}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onNodeClick={onNodeClick}
|
||||
onMouseMove={onMouseMove}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onReconnect={onReconnect}
|
||||
onReconnectStart={onReconnectStart}
|
||||
onReconnectEnd={onReconnectEnd}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnect={onConnect}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onMoveEnd={handleMoveEnd}
|
||||
connectionLineComponent={CustomConnectionLine}
|
||||
isValidConnection={isValidConnection}
|
||||
edgesFocusable={!isLocked}
|
||||
edgesReconnectable={!isLocked}
|
||||
nodesDraggable={!isLocked}
|
||||
nodesConnectable={!isLocked}
|
||||
nodesFocusable={!isLocked}
|
||||
elementsSelectable={!isLocked}
|
||||
minZoom={0.1}
|
||||
snapToGrid={shouldSnapToGrid}
|
||||
snapGrid={snapGrid}
|
||||
connectionRadius={30}
|
||||
proOptions={proOptions}
|
||||
style={flowStyles}
|
||||
onPaneClick={handlePaneClick}
|
||||
deleteKeyCode={null}
|
||||
selectionMode={selectionMode}
|
||||
elevateEdgesOnSelect
|
||||
nodeDragThreshold={1}
|
||||
noDragClassName={NO_DRAG_CLASS}
|
||||
noWheelClassName={NO_WHEEL_CLASS}
|
||||
noPanClassName={NO_PAN_CLASS}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
Flow.displayName = 'Flow';
|
||||
HotkeyIsolator.displayName = 'HotkeyIsolator';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectNodes } from 'features/nodes/store/selectors';
|
||||
import type { Templates } from 'features/nodes/store/types';
|
||||
import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
@@ -8,9 +8,9 @@ import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { getFieldColor } from './getEdgeColor';
|
||||
|
||||
export const buildSelectAreConnectedNodesSelected = (source: string, target: string) =>
|
||||
createSelector(selectNodesSlice, (nodes): boolean => {
|
||||
const sourceNode = nodes.nodes.find((node) => node.id === source);
|
||||
const targetNode = nodes.nodes.find((node) => node.id === target);
|
||||
createSelector(selectNodes, (nodes): boolean => {
|
||||
const sourceNode = nodes.find((node) => node.id === source);
|
||||
const targetNode = nodes.find((node) => node.id === target);
|
||||
|
||||
return Boolean(sourceNode?.selected || targetNode?.selected);
|
||||
});
|
||||
@@ -22,10 +22,13 @@ export const buildSelectEdgeColor = (
|
||||
target: string,
|
||||
targetHandleId: string | null | undefined
|
||||
) =>
|
||||
createSelector(selectNodesSlice, selectWorkflowSettingsSlice, (nodes, workflowSettings): string => {
|
||||
createSelector(selectNodes, selectWorkflowSettingsSlice, (nodes, workflowSettings): string => {
|
||||
const { shouldColorEdges } = workflowSettings;
|
||||
const sourceNode = nodes.nodes.find((node) => node.id === source);
|
||||
const targetNode = nodes.nodes.find((node) => node.id === target);
|
||||
if (!shouldColorEdges) {
|
||||
return colorTokenToCssVar('base.500');
|
||||
}
|
||||
const sourceNode = nodes.find((node) => node.id === source);
|
||||
const targetNode = nodes.find((node) => node.id === target);
|
||||
|
||||
if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) {
|
||||
return colorTokenToCssVar('base.500');
|
||||
@@ -37,7 +40,7 @@ export const buildSelectEdgeColor = (
|
||||
const outputFieldTemplate = sourceNodeTemplate?.outputs[sourceHandleId];
|
||||
const sourceType = isInvocationToInvocationEdge ? outputFieldTemplate?.type : undefined;
|
||||
|
||||
return sourceType && shouldColorEdges ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
|
||||
return sourceType ? getFieldColor(sourceType) : colorTokenToCssVar('base.500');
|
||||
});
|
||||
|
||||
export const buildSelectEdgeLabel = (
|
||||
@@ -47,9 +50,9 @@ export const buildSelectEdgeLabel = (
|
||||
target: string,
|
||||
targetHandleId: string | null | undefined
|
||||
) =>
|
||||
createSelector(selectNodesSlice, (nodes): string | null => {
|
||||
const sourceNode = nodes.nodes.find((node) => node.id === source);
|
||||
const targetNode = nodes.nodes.find((node) => node.id === target);
|
||||
createSelector(selectNodes, (nodes): string | null => {
|
||||
const sourceNode = nodes.find((node) => node.id === source);
|
||||
const targetNode = nodes.find((node) => node.id === target);
|
||||
|
||||
if (!sourceNode || !sourceHandleId || !targetNode || !targetHandleId) {
|
||||
return null;
|
||||
|
||||
@@ -37,7 +37,7 @@ const sx: SystemStyleObject = {
|
||||
};
|
||||
|
||||
const InvocationNode = ({ nodeId, isOpen }: Props) => {
|
||||
const withFooter = useWithFooter(nodeId);
|
||||
const withFooter = useWithFooter();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -64,7 +64,7 @@ const InvocationNode = ({ nodeId, isOpen }: Props) => {
|
||||
export default memo(InvocationNode);
|
||||
|
||||
const ConnectionFields = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const fieldNames = useInputFieldNamesConnection(nodeId);
|
||||
const fieldNames = useInputFieldNamesConnection();
|
||||
return (
|
||||
<>
|
||||
{fieldNames.map((fieldName, i) => (
|
||||
@@ -80,7 +80,7 @@ const ConnectionFields = memo(({ nodeId }: { nodeId: string }) => {
|
||||
ConnectionFields.displayName = 'ConnectionFields';
|
||||
|
||||
const AnyOrDirectFields = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const fieldNames = useInputFieldNamesAnyOrDirect(nodeId);
|
||||
const fieldNames = useInputFieldNamesAnyOrDirect();
|
||||
return (
|
||||
<>
|
||||
{fieldNames.map((fieldName) => (
|
||||
@@ -94,7 +94,7 @@ const AnyOrDirectFields = memo(({ nodeId }: { nodeId: string }) => {
|
||||
AnyOrDirectFields.displayName = 'AnyOrDirectFields';
|
||||
|
||||
const MissingFields = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const fieldNames = useInputFieldNamesMissing(nodeId);
|
||||
const fieldNames = useInputFieldNamesMissing();
|
||||
return (
|
||||
<>
|
||||
{fieldNames.map((fieldName) => (
|
||||
@@ -108,7 +108,7 @@ const MissingFields = memo(({ nodeId }: { nodeId: string }) => {
|
||||
MissingFields.displayName = 'MissingFields';
|
||||
|
||||
const OutputFields = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const fieldNames = useOutputFieldNames(nodeId);
|
||||
const fieldNames = useOutputFieldNames();
|
||||
return (
|
||||
<>
|
||||
{fieldNames.map((fieldName, i) => (
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const InvocationNodeClassificationIcon = ({ nodeId }: Props) => {
|
||||
const classification = useNodeClassification(nodeId);
|
||||
const classification = useNodeClassification();
|
||||
|
||||
if (!classification || classification === 'stable') {
|
||||
return null;
|
||||
|
||||
@@ -19,7 +19,7 @@ const collapsedHandleStyles: CSSProperties = {
|
||||
};
|
||||
|
||||
const InvocationNodeCollapsedHandles = ({ nodeId }: Props) => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const template = useNodeTemplateOrThrow();
|
||||
|
||||
if (!template) {
|
||||
return null;
|
||||
|
||||
@@ -16,8 +16,8 @@ type Props = {
|
||||
const props: ChakraProps = { w: 'unset' };
|
||||
|
||||
const InvocationNodeFooter = ({ nodeId }: Props) => {
|
||||
const hasImageOutput = useNodeHasImageOutput(nodeId);
|
||||
const isExecutableNode = useIsExecutableNode(nodeId);
|
||||
const hasImageOutput = useNodeHasImageOutput();
|
||||
const isExecutableNode = useIsExecutableNode();
|
||||
const isCacheEnabled = useFeatureStatus('invocationCache');
|
||||
return (
|
||||
<Flex
|
||||
|
||||
@@ -32,7 +32,7 @@ const sx: SystemStyleObject = {
|
||||
};
|
||||
|
||||
const InvocationNodeHeader = ({ nodeId, isOpen }: Props) => {
|
||||
const isInvalid = useNodeIsInvalid(nodeId);
|
||||
const isInvalid = useNodeIsInvalid();
|
||||
|
||||
return (
|
||||
<Flex layerStyle="nodeHeader" sx={sx} data-is-open={isOpen} data-is-invalid={isInvalid}>
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => {
|
||||
const needsUpdate = useNodeNeedsUpdate(nodeId);
|
||||
const needsUpdate = useNodeNeedsUpdate();
|
||||
|
||||
return (
|
||||
<Tooltip label={<TooltipContent nodeId={nodeId} />} placement="top" shouldWrapChildren>
|
||||
@@ -26,10 +26,10 @@ export const InvocationNodeInfoIcon = memo(({ nodeId }: Props) => {
|
||||
InvocationNodeInfoIcon.displayName = 'InvocationNodeInfoIcon';
|
||||
|
||||
const TooltipContent = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const notes = useInvocationNodeNotes(nodeId);
|
||||
const label = useNodeUserTitleSafe(nodeId);
|
||||
const version = useNodeVersion(nodeId);
|
||||
const nodeTemplate = useNodeTemplateOrThrow(nodeId);
|
||||
const notes = useInvocationNodeNotes();
|
||||
const label = useNodeUserTitleSafe();
|
||||
const version = useNodeVersion();
|
||||
const nodeTemplate = useNodeTemplateOrThrow();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const title = useMemo(() => {
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
export const InvocationNodeNotesTextarea = memo(({ nodeId }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const notes = useInvocationNodeNotes(nodeId);
|
||||
const notes = useInvocationNodeNotes();
|
||||
const handleNotesChanged = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(nodeNotesChanged({ nodeId, notes: e.target.value }));
|
||||
|
||||
@@ -14,7 +14,7 @@ type Props = {
|
||||
|
||||
const InvocationNodeUnknownFallback = ({ nodeId, isOpen, label, type }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const nodePack = useNodePack(nodeId);
|
||||
const nodePack = useNodePack();
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useTranslation } from 'react-i18next';
|
||||
const SaveToGalleryCheckbox = ({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const hasImageOutput = useNodeHasImageOutput(nodeId);
|
||||
const isIntermediate = useNodeIsIntermediate(nodeId);
|
||||
const hasImageOutput = useNodeHasImageOutput();
|
||||
const isIntermediate = useNodeIsIntermediate();
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
const UseCacheCheckbox = ({ nodeId }: { nodeId: string }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const useCache = useUseCache(nodeId);
|
||||
const useCache = useUseCache();
|
||||
const handleChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type { Selector } from '@reduxjs/toolkit';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectEdges, selectNodes } from 'features/nodes/store/selectors';
|
||||
import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { createContext, memo, useContext, useMemo } from 'react';
|
||||
|
||||
type InvocationNodeContextValue = {
|
||||
nodeId: string;
|
||||
|
||||
selectNodeSafe: Selector<RootState, InvocationNode | null>;
|
||||
selectNodeDataSafe: Selector<RootState, InvocationNode['data'] | null>;
|
||||
selectNodeTypeSafe: Selector<RootState, string | null>;
|
||||
selectNodeTemplateSafe: Selector<RootState, InvocationTemplate | null>;
|
||||
selectNodeInputsSafe: Selector<RootState, InvocationNode['data']['inputs'] | null>;
|
||||
|
||||
buildSelectInputFieldSafe: (
|
||||
fieldName: string
|
||||
) => Selector<RootState, InvocationNode['data']['inputs'][string] | null>;
|
||||
buildSelectInputFieldTemplateSafe: (
|
||||
fieldName: string
|
||||
) => Selector<RootState, InvocationTemplate['inputs'][string] | null>;
|
||||
buildSelectOutputFieldTemplateSafe: (
|
||||
fieldName: string
|
||||
) => Selector<RootState, InvocationTemplate['outputs'][string] | null>;
|
||||
|
||||
selectNodeOrThrow: Selector<RootState, InvocationNode>;
|
||||
selectNodeDataOrThrow: Selector<RootState, InvocationNode['data']>;
|
||||
selectNodeTypeOrThrow: Selector<RootState, string>;
|
||||
selectNodeTemplateOrThrow: Selector<RootState, InvocationTemplate>;
|
||||
selectNodeInputsOrThrow: Selector<RootState, InvocationNode['data']['inputs']>;
|
||||
|
||||
buildSelectInputFieldOrThrow: (fieldName: string) => Selector<RootState, InvocationNode['data']['inputs'][string]>;
|
||||
buildSelectInputFieldTemplateOrThrow: (
|
||||
fieldName: string
|
||||
) => Selector<RootState, InvocationTemplate['inputs'][string]>;
|
||||
buildSelectOutputFieldTemplateOrThrow: (
|
||||
fieldName: string
|
||||
) => Selector<RootState, InvocationTemplate['outputs'][string]>;
|
||||
|
||||
buildSelectIsInputFieldConnected: (fieldName: string) => Selector<RootState, boolean>;
|
||||
};
|
||||
|
||||
const InvocationNodeContext = createContext<InvocationNodeContextValue | null>(null);
|
||||
|
||||
const getSelectorFromCache = <T,>(
|
||||
cache: Map<string, Selector<RootState, T>>,
|
||||
key: string,
|
||||
fallback: () => Selector<RootState, T>
|
||||
): Selector<RootState, T> => {
|
||||
let selector = cache.get(key);
|
||||
if (!selector) {
|
||||
selector = fallback();
|
||||
cache.set(key, selector);
|
||||
}
|
||||
return selector;
|
||||
};
|
||||
|
||||
export const InvocationNodeContextProvider = memo(({ nodeId, children }: PropsWithChildren<{ nodeId: string }>) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
const value = useMemo(() => {
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const cache: Map<string, Selector<RootState, any>> = new Map();
|
||||
|
||||
const selectNodeSafe = getSelectorFromCache(cache, 'selectNodeSafe', () =>
|
||||
createSelector(selectNodes, (nodes) => {
|
||||
return (nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) ?? null) as InvocationNode | null;
|
||||
})
|
||||
);
|
||||
const selectNodeDataSafe = getSelectorFromCache(cache, 'selectNodeDataSafe', () =>
|
||||
createSelector(selectNodeSafe, (node) => {
|
||||
return node?.data ?? null;
|
||||
})
|
||||
);
|
||||
const selectNodeTypeSafe = getSelectorFromCache(cache, 'selectNodeTypeSafe', () =>
|
||||
createSelector(selectNodeDataSafe, (data) => {
|
||||
return data?.type ?? null;
|
||||
})
|
||||
);
|
||||
const selectNodeTemplateSafe = getSelectorFromCache(cache, 'selectNodeTemplateSafe', () =>
|
||||
createSelector(selectNodeTypeSafe, (type) => {
|
||||
return type ? (templates[type] ?? null) : null;
|
||||
})
|
||||
);
|
||||
const selectNodeInputsSafe = getSelectorFromCache(cache, 'selectNodeInputsSafe', () =>
|
||||
createSelector(selectNodeDataSafe, (data) => {
|
||||
return data?.inputs ?? null;
|
||||
})
|
||||
);
|
||||
const buildSelectInputFieldSafe = (fieldName: string) =>
|
||||
getSelectorFromCache(cache, `buildSelectInputFieldSafe-${fieldName}`, () =>
|
||||
createSelector(selectNodeInputsSafe, (inputs) => {
|
||||
return inputs?.[fieldName] ?? null;
|
||||
})
|
||||
);
|
||||
const buildSelectInputFieldTemplateSafe = (fieldName: string) =>
|
||||
getSelectorFromCache(cache, `buildSelectInputFieldTemplateSafe-${fieldName}`, () =>
|
||||
createSelector(selectNodeTemplateSafe, (template) => {
|
||||
return template?.inputs?.[fieldName] ?? null;
|
||||
})
|
||||
);
|
||||
const buildSelectOutputFieldTemplateSafe = (fieldName: string) =>
|
||||
getSelectorFromCache(cache, `buildSelectOutputFieldTemplateSafe-${fieldName}`, () =>
|
||||
createSelector(selectNodeTemplateSafe, (template) => {
|
||||
return template?.outputs?.[fieldName] ?? null;
|
||||
})
|
||||
);
|
||||
|
||||
const selectNodeOrThrow = getSelectorFromCache(cache, 'selectNodeOrThrow', () =>
|
||||
createSelector(selectNodes, (nodes) => {
|
||||
const node = nodes.find(({ id, type }) => type === 'invocation' && id === nodeId) as InvocationNode | undefined;
|
||||
if (node === undefined) {
|
||||
throw new Error(`Cannot find node with id ${nodeId}`);
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
const selectNodeDataOrThrow = getSelectorFromCache(cache, 'selectNodeDataOrThrow', () =>
|
||||
createSelector(selectNodeOrThrow, (node) => {
|
||||
return node.data;
|
||||
})
|
||||
);
|
||||
const selectNodeTypeOrThrow = getSelectorFromCache(cache, 'selectNodeTypeOrThrow', () =>
|
||||
createSelector(selectNodeDataOrThrow, (data) => {
|
||||
return data.type;
|
||||
})
|
||||
);
|
||||
const selectNodeTemplateOrThrow = getSelectorFromCache(cache, 'selectNodeTemplateOrThrow', () =>
|
||||
createSelector(selectNodeTypeOrThrow, (type) => {
|
||||
const template = templates[type];
|
||||
if (template === undefined) {
|
||||
throw new Error(`Cannot find template for node with id ${nodeId} with type ${type}`);
|
||||
}
|
||||
return template;
|
||||
})
|
||||
);
|
||||
const selectNodeInputsOrThrow = getSelectorFromCache(cache, 'selectNodeInputsOrThrow', () =>
|
||||
createSelector(selectNodeDataOrThrow, (data) => {
|
||||
return data.inputs;
|
||||
})
|
||||
);
|
||||
const buildSelectInputFieldOrThrow = (fieldName: string) =>
|
||||
getSelectorFromCache(cache, `buildSelectInputFieldOrThrow-${fieldName}`, () =>
|
||||
createSelector(selectNodeInputsOrThrow, (inputs) => {
|
||||
const field = inputs[fieldName];
|
||||
if (field === undefined) {
|
||||
throw new Error(`Cannot find input field with name ${fieldName} in node ${nodeId}`);
|
||||
}
|
||||
return field;
|
||||
})
|
||||
);
|
||||
const buildSelectInputFieldTemplateOrThrow = (fieldName: string) =>
|
||||
getSelectorFromCache(cache, `buildSelectInputFieldTemplateOrThrow-${fieldName}`, () =>
|
||||
createSelector(selectNodeTemplateOrThrow, (template) => {
|
||||
const fieldTemplate = template.inputs[fieldName];
|
||||
if (fieldTemplate === undefined) {
|
||||
throw new Error(`Cannot find input field template with name ${fieldName} in node ${nodeId}`);
|
||||
}
|
||||
return fieldTemplate;
|
||||
})
|
||||
);
|
||||
const buildSelectOutputFieldTemplateOrThrow = (fieldName: string) =>
|
||||
getSelectorFromCache(cache, `buildSelectOutputFieldTemplateOrThrow-${fieldName}`, () =>
|
||||
createSelector(selectNodeTemplateOrThrow, (template) => {
|
||||
const fieldTemplate = template.outputs[fieldName];
|
||||
if (fieldTemplate === undefined) {
|
||||
throw new Error(`Cannot find output field template with name ${fieldName} in node ${nodeId}`);
|
||||
}
|
||||
return fieldTemplate;
|
||||
})
|
||||
);
|
||||
|
||||
const buildSelectIsInputFieldConnected = (fieldName: string) =>
|
||||
getSelectorFromCache(cache, `buildSelectIsInputFieldConnected-${fieldName}`, () =>
|
||||
createSelector(selectEdges, (edges) => {
|
||||
return edges.some((edge) => {
|
||||
return edge.target === nodeId && edge.targetHandle === fieldName;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
nodeId,
|
||||
|
||||
selectNodeSafe,
|
||||
selectNodeDataSafe,
|
||||
selectNodeTypeSafe,
|
||||
selectNodeTemplateSafe,
|
||||
selectNodeInputsSafe,
|
||||
|
||||
buildSelectInputFieldSafe,
|
||||
buildSelectInputFieldTemplateSafe,
|
||||
buildSelectOutputFieldTemplateSafe,
|
||||
|
||||
selectNodeOrThrow,
|
||||
selectNodeDataOrThrow,
|
||||
selectNodeTypeOrThrow,
|
||||
selectNodeTemplateOrThrow,
|
||||
selectNodeInputsOrThrow,
|
||||
|
||||
buildSelectInputFieldOrThrow,
|
||||
buildSelectInputFieldTemplateOrThrow,
|
||||
buildSelectOutputFieldTemplateOrThrow,
|
||||
|
||||
buildSelectIsInputFieldConnected,
|
||||
} satisfies InvocationNodeContextValue;
|
||||
}, [nodeId, templates]);
|
||||
|
||||
return <InvocationNodeContext.Provider value={value}>{children}</InvocationNodeContext.Provider>;
|
||||
});
|
||||
|
||||
export const useInvocationNodeContext = () => {
|
||||
const context = useContext(InvocationNodeContext);
|
||||
if (!context) {
|
||||
throw new Error('useInvocationNodeContext must be used within an InvocationNodeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -48,7 +48,7 @@ InputFieldDescriptionPopover.displayName = 'InputFieldDescriptionPopover';
|
||||
const Content = memo(({ nodeId, fieldName }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const description = useInputFieldUserDescriptionSafe(nodeId, fieldName);
|
||||
const description = useInputFieldUserDescriptionSafe(fieldName);
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(fieldDescriptionChanged({ nodeId, fieldName, val: e.target.value }));
|
||||
|
||||
@@ -22,9 +22,9 @@ interface Props {
|
||||
}
|
||||
|
||||
export const InputFieldEditModeNodes = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, fieldName);
|
||||
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldName);
|
||||
const isInvalid = useInputFieldIsInvalid(fieldName);
|
||||
const isConnected = useInputFieldIsConnected(fieldName);
|
||||
|
||||
if (fieldTemplate.input === 'connection' || isConnected) {
|
||||
return (
|
||||
|
||||
@@ -15,8 +15,8 @@ type Props = PropsWithChildren<{
|
||||
}>;
|
||||
|
||||
export const InputFieldGate = memo(({ nodeId, fieldName, children, fallback, formatLabel }: Props) => {
|
||||
const hasInstance = useInputFieldInstanceExists(nodeId, fieldName);
|
||||
const hasTemplate = useInputFieldTemplateExists(nodeId, fieldName);
|
||||
const hasInstance = useInputFieldInstanceExists(fieldName);
|
||||
const hasTemplate = useInputFieldTemplateExists(fieldName);
|
||||
|
||||
if (!hasTemplate || !hasInstance) {
|
||||
// fallback may be null, indicating we should render nothing at all - must check for undefined explicitly
|
||||
@@ -54,7 +54,7 @@ const Fallback = memo(
|
||||
hasInstance: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useInputFieldNameSafe(nodeId, fieldName);
|
||||
const name = useInputFieldNameSafe(fieldName);
|
||||
const label = useMemo(() => {
|
||||
if (formatLabel) {
|
||||
return formatLabel(name);
|
||||
|
||||
@@ -63,7 +63,7 @@ const handleStyles = {
|
||||
} satisfies CSSProperties;
|
||||
|
||||
export const InputFieldHandle = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldName);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
|
||||
const isModelField = useMemo(() => isModelFieldType(fieldTemplate.type), [fieldTemplate.type]);
|
||||
|
||||
@@ -150,8 +150,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export const InputFieldRenderer = memo(({ nodeId, fieldName, settings }: Props) => {
|
||||
const field = useInputFieldInstance(nodeId, fieldName);
|
||||
const template = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const field = useInputFieldInstance(fieldName);
|
||||
const template = useInputFieldTemplateOrThrow(fieldName);
|
||||
|
||||
// When deciding which component to render, first we check the type of the template, which is more efficient than the
|
||||
// instance type check. The instance type check uses zod and is slower.
|
||||
|
||||
@@ -11,7 +11,7 @@ type Props = {
|
||||
|
||||
export const InputFieldResetToDefaultValueIconButton = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isValueChanged, resetToDefaultValue } = useInputFieldDefaultValue(nodeId, fieldName);
|
||||
const { isValueChanged, resetToDefaultValue } = useInputFieldDefaultValue(fieldName);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
|
||||
@@ -43,10 +43,10 @@ interface Props {
|
||||
export const InputFieldTitle = memo((props: Props) => {
|
||||
const { nodeId, fieldName, isInvalid, isDragging } = props;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const label = useInputFieldUserTitleSafe(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(nodeId, fieldName);
|
||||
const label = useInputFieldUserTitleSafe(fieldName);
|
||||
const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(fieldName);
|
||||
const { t } = useTranslation();
|
||||
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
|
||||
const isConnected = useInputFieldIsConnected(fieldName);
|
||||
const isConnectionStartField = useIsConnectionStartField(nodeId, fieldName, 'target');
|
||||
const isConnectionInProgress = useIsConnectionInProgress();
|
||||
const connectionError = useConnectionErrorTKey(nodeId, fieldName, 'target');
|
||||
|
||||
@@ -15,10 +15,10 @@ interface Props {
|
||||
export const InputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const fieldInstance = useInputFieldInstance(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const fieldInstance = useInputFieldInstance(fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldName);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const fieldErrors = useInputFieldErrors(nodeId, fieldName);
|
||||
const fieldErrors = useInputFieldErrors(fieldName);
|
||||
|
||||
const fieldTitle = useMemo(() => {
|
||||
if (fieldInstance.label && fieldTemplate.title) {
|
||||
|
||||
@@ -12,7 +12,7 @@ type Props = PropsWithChildren<{
|
||||
}>;
|
||||
|
||||
export const OutputFieldGate = memo(({ nodeId, fieldName, children }: Props) => {
|
||||
const hasTemplate = useOutputFieldTemplateExists(nodeId, fieldName);
|
||||
const hasTemplate = useOutputFieldTemplateExists(fieldName);
|
||||
|
||||
if (!hasTemplate) {
|
||||
return <Fallback nodeId={nodeId} fieldName={fieldName} />;
|
||||
@@ -25,7 +25,7 @@ OutputFieldGate.displayName = 'OutputFieldGate';
|
||||
|
||||
const Fallback = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const name = useOutputFieldName(nodeId, fieldName);
|
||||
const name = useOutputFieldName(fieldName);
|
||||
|
||||
return (
|
||||
<OutputFieldWrapper>
|
||||
|
||||
@@ -63,7 +63,7 @@ const handleStyles = {
|
||||
} satisfies CSSProperties;
|
||||
|
||||
export const OutputFieldHandle = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTemplate = useOutputFieldTemplate(fieldName);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const fieldColor = useMemo(() => getFieldColor(fieldTemplate.type), [fieldTemplate.type]);
|
||||
const isModelField = useMemo(() => isModelFieldType(fieldTemplate.type), [fieldTemplate.type]);
|
||||
|
||||
@@ -27,8 +27,8 @@ type Props = {
|
||||
};
|
||||
|
||||
export const OutputFieldTitle = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
|
||||
const isConnected = useInputFieldIsConnected(nodeId, fieldName);
|
||||
const fieldTemplate = useOutputFieldTemplate(fieldName);
|
||||
const isConnected = useInputFieldIsConnected(fieldName);
|
||||
const isConnectionStartField = useIsConnectionStartField(nodeId, fieldName, 'source');
|
||||
const isConnectionInProgress = useIsConnectionInProgress();
|
||||
const connectionErrorTKey = useConnectionErrorTKey(nodeId, fieldName, 'source');
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const OutputFieldTooltipContent = memo(({ nodeId, fieldName }: Props) => {
|
||||
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
|
||||
const fieldTemplate = useOutputFieldTemplate(fieldName);
|
||||
const fieldTypeName = useFieldTypeName(fieldTemplate.type);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export const FloatFieldCollectionInputComponent = memo(
|
||||
const store = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
|
||||
const isInvalid = useInputFieldIsInvalid(field.name);
|
||||
|
||||
const onChangeValue = useCallback(
|
||||
(value: FloatFieldCollectionInputInstance['value']) => {
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ImageFieldCollectionInputComponent = memo(
|
||||
const { nodeId, field } = props;
|
||||
const store = useAppStore();
|
||||
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
|
||||
const isInvalid = useInputFieldIsInvalid(field.name);
|
||||
|
||||
const dndTargetData = useMemo<AddImagesToNodeImageFieldCollection>(
|
||||
() =>
|
||||
|
||||
@@ -43,7 +43,7 @@ export const IntegerFieldCollectionInputComponent = memo(
|
||||
const store = useAppStore();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
|
||||
const isInvalid = useInputFieldIsInvalid(field.name);
|
||||
|
||||
const onChangeValue = useCallback(
|
||||
(value: IntegerFieldCollectionInputInstance['value']) => {
|
||||
|
||||
@@ -33,7 +33,7 @@ export const StringFieldCollectionInputComponent = memo(
|
||||
const { t } = useTranslation();
|
||||
const store = useAppStore();
|
||||
|
||||
const isInvalid = useInputFieldIsInvalid(nodeId, field.name);
|
||||
const isInvalid = useInputFieldIsInvalid(field.name);
|
||||
|
||||
const onRemoveString = useCallback(
|
||||
(index: number) => {
|
||||
|
||||
@@ -17,10 +17,10 @@ type Props = {
|
||||
|
||||
const NodeTitle = ({ nodeId, title }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const label = useNodeUserTitleSafe(nodeId);
|
||||
const label = useNodeUserTitleSafe();
|
||||
const batchGroupId = useBatchGroupId(nodeId);
|
||||
const batchGroupColorToken = useBatchGroupColorToken(batchGroupId);
|
||||
const templateTitle = useNodeTemplateTitleSafe(nodeId);
|
||||
const templateTitle = useNodeTemplateTitleSafe();
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ChakraProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, useGlobalMenuClose } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useIsWorkflowEditorLocked } from 'features/nodes/hooks/useIsWorkflowEditorLocked';
|
||||
import { useMouseOverFormField, useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { useNodeExecutionState } from 'features/nodes/hooks/useNodeExecutionState';
|
||||
@@ -137,24 +138,26 @@ const NodeWrapper = (props: NodeWrapperProps) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={globalMenu.onCloseGlobal}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onMouseOver={mouseOverNode.handleMouseOver}
|
||||
onMouseOut={mouseOverNode.handleMouseOut}
|
||||
className={DRAG_HANDLE_CLASSNAME}
|
||||
sx={containerSx}
|
||||
width={width || NODE_WIDTH}
|
||||
opacity={opacity}
|
||||
data-is-editor-locked={isLocked}
|
||||
data-is-selected={selected}
|
||||
data-is-mouse-over-form-field={mouseOverFormField.isMouseOverFormField}
|
||||
>
|
||||
<Box sx={shadowsSx} />
|
||||
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
|
||||
{children}
|
||||
<Box className="node-selection-overlay" />
|
||||
</Box>
|
||||
<InvocationNodeContextProvider nodeId={nodeId}>
|
||||
<Box
|
||||
onClick={globalMenu.onCloseGlobal}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onMouseOver={mouseOverNode.handleMouseOver}
|
||||
onMouseOut={mouseOverNode.handleMouseOut}
|
||||
className={DRAG_HANDLE_CLASSNAME}
|
||||
sx={containerSx}
|
||||
width={width || NODE_WIDTH}
|
||||
opacity={opacity}
|
||||
data-is-editor-locked={isLocked}
|
||||
data-is-selected={selected}
|
||||
data-is-mouse-over-form-field={mouseOverFormField.isMouseOverFormField}
|
||||
>
|
||||
<Box sx={shadowsSx} />
|
||||
<Box sx={inProgressSx} data-is-in-progress={isInProgress} />
|
||||
{children}
|
||||
<Box className="node-selection-overlay" />
|
||||
</Box>
|
||||
</InvocationNodeContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { FlexProps, SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { camelCase } from 'es-toolkit/compat';
|
||||
import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { ContainerElementSettings } from 'features/nodes/components/sidePanel/builder/ContainerElementSettings';
|
||||
import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
@@ -49,14 +50,16 @@ export const FormElementEditModeHeader = memo(({ element, dragHandleRef, ...rest
|
||||
<Spacer />
|
||||
{isContainerElement(element) && <ContainerElementSettings element={element} />}
|
||||
{isNodeFieldElement(element) && (
|
||||
<InputFieldGate
|
||||
nodeId={element.data.fieldIdentifier.nodeId}
|
||||
fieldName={element.data.fieldIdentifier.fieldName}
|
||||
fallback={null} // Do not render these buttons if the field is not found
|
||||
>
|
||||
<ZoomToNodeButton element={element} />
|
||||
<NodeFieldElementSettings element={element} />
|
||||
</InputFieldGate>
|
||||
<InvocationNodeContextProvider nodeId={element.data.fieldIdentifier.nodeId}>
|
||||
<InputFieldGate
|
||||
nodeId={element.data.fieldIdentifier.nodeId}
|
||||
fieldName={element.data.fieldIdentifier.fieldName}
|
||||
fallback={null} // Do not render these buttons if the field is not found
|
||||
>
|
||||
<ZoomToNodeButton element={element} />
|
||||
<NodeFieldElementSettings element={element} />
|
||||
</InputFieldGate>
|
||||
</InvocationNodeContextProvider>
|
||||
)}
|
||||
<RemoveElementButton element={element} />
|
||||
</Flex>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { NodeFieldElementEditMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementEditMode';
|
||||
import { NodeFieldElementViewMode } from 'features/nodes/components/sidePanel/builder/NodeFieldElementViewMode';
|
||||
import { useElement } from 'features/nodes/components/sidePanel/builder/use-element';
|
||||
@@ -15,11 +16,19 @@ export const NodeFieldElement = memo(({ id }: { id: string }) => {
|
||||
}
|
||||
|
||||
if (mode === 'view') {
|
||||
return <NodeFieldElementViewMode el={el} />;
|
||||
return (
|
||||
<InvocationNodeContextProvider nodeId={el.data.fieldIdentifier.nodeId}>
|
||||
<NodeFieldElementViewMode el={el} />
|
||||
</InvocationNodeContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// mode === 'edit'
|
||||
return <NodeFieldElementEditMode el={el} />;
|
||||
return (
|
||||
<InvocationNodeContextProvider nodeId={el.data.fieldIdentifier.nodeId}>
|
||||
<NodeFieldElementEditMode el={el} />
|
||||
</InvocationNodeContextProvider>
|
||||
);
|
||||
});
|
||||
|
||||
NodeFieldElement.displayName = 'NodeFieldElement';
|
||||
|
||||
@@ -13,8 +13,8 @@ export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeField
|
||||
const { data } = el;
|
||||
const { fieldIdentifier } = data;
|
||||
const dispatch = useAppDispatch();
|
||||
const description = useInputFieldUserDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const description = useInputFieldUserDescriptionSafe(fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.fieldName);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const onChange = useCallback(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Divider, Flex, FormControl } from '@invoke-ai/ui-library';
|
||||
import { InvocationNodeContextProvider } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { InputFieldGate } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldGate';
|
||||
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
||||
import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
@@ -63,25 +64,27 @@ const NodeFieldElementEditModeContent = memo(
|
||||
<>
|
||||
<FormElementEditModeHeader dragHandleRef={dragHandleRef} element={el} data-is-dragging={isDragging} />
|
||||
<FormElementEditModeContent data-is-dragging={isDragging} p={4}>
|
||||
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<FormControl flex="1 1 0" orientation="vertical">
|
||||
<NodeFieldElementLabelEditable el={el} />
|
||||
<Flex w="full" gap={4}>
|
||||
<InputFieldRenderer
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
settings={data.settings}
|
||||
/>
|
||||
</Flex>
|
||||
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
|
||||
{data.settings?.type === 'string-field-config' && data.settings.component === 'dropdown' && (
|
||||
<>
|
||||
<Divider />
|
||||
<NodeFieldElementStringDropdownSettings id={id} settings={data.settings} />
|
||||
</>
|
||||
)}
|
||||
</FormControl>
|
||||
</InputFieldGate>
|
||||
<InvocationNodeContextProvider nodeId={fieldIdentifier.nodeId}>
|
||||
<InputFieldGate nodeId={fieldIdentifier.nodeId} fieldName={fieldIdentifier.fieldName}>
|
||||
<FormControl flex="1 1 0" orientation="vertical">
|
||||
<NodeFieldElementLabelEditable el={el} />
|
||||
<Flex w="full" gap={4}>
|
||||
<InputFieldRenderer
|
||||
nodeId={fieldIdentifier.nodeId}
|
||||
fieldName={fieldIdentifier.fieldName}
|
||||
settings={data.settings}
|
||||
/>
|
||||
</Flex>
|
||||
{showDescription && <NodeFieldElementDescriptionEditable el={el} />}
|
||||
{data.settings?.type === 'string-field-config' && data.settings.component === 'dropdown' && (
|
||||
<>
|
||||
<Divider />
|
||||
<NodeFieldElementStringDropdownSettings id={id} settings={data.settings} />
|
||||
</>
|
||||
)}
|
||||
</FormControl>
|
||||
</InputFieldGate>
|
||||
</InvocationNodeContextProvider>
|
||||
</FormElementEditModeContent>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ SettingComponent.displayName = 'SettingComponent';
|
||||
const SettingMin = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
|
||||
const field = useInputFieldInstance<FloatFieldInputInstance>(fieldName);
|
||||
|
||||
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
@@ -129,7 +129,7 @@ SettingMin.displayName = 'SettingMin';
|
||||
const SettingMax = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<FloatFieldInputInstance>(nodeId, fieldName);
|
||||
const field = useInputFieldInstance<FloatFieldInputInstance>(fieldName);
|
||||
|
||||
const floatField = useFloatField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ SettingComponent.displayName = 'SettingComponent';
|
||||
const SettingMin = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
|
||||
const field = useInputFieldInstance<IntegerFieldInputInstance>(fieldName);
|
||||
|
||||
const integerField = useIntegerField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
@@ -131,7 +131,7 @@ SettingMin.displayName = 'SettingMin';
|
||||
const SettingMax = memo(({ id, settings, nodeId, fieldName, fieldTemplate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const field = useInputFieldInstance<IntegerFieldInputInstance>(nodeId, fieldName);
|
||||
const field = useInputFieldInstance<IntegerFieldInputInstance>(fieldName);
|
||||
|
||||
const integerField = useIntegerField(nodeId, fieldName, fieldTemplate);
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { memo, useMemo } from 'react';
|
||||
export const NodeFieldElementLabel = memo(({ el }: { el: NodeFieldElement }) => {
|
||||
const { data } = el;
|
||||
const { fieldIdentifier } = data;
|
||||
const label = useInputFieldUserTitleSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const label = useInputFieldUserTitleSafe(fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.fieldName);
|
||||
|
||||
const _label = useMemo(() => label || fieldTemplate.title, [label, fieldTemplate.title]);
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ export const NodeFieldElementLabelEditable = memo(({ el }: { el: NodeFieldElemen
|
||||
const { data } = el;
|
||||
const { fieldIdentifier } = data;
|
||||
const dispatch = useAppDispatch();
|
||||
const label = useInputFieldUserTitleSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const label = useInputFieldUserTitleSafe(fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.fieldName);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onChange = useCallback(
|
||||
|
||||
@@ -36,7 +36,7 @@ export const NodeFieldElementSettings = memo(({ element }: { element: NodeFieldE
|
||||
const { id, data } = element;
|
||||
const { showDescription, fieldIdentifier } = data;
|
||||
const { nodeId, fieldName } = fieldIdentifier;
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldName);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -37,8 +37,8 @@ const useFormatFallbackLabel = () => {
|
||||
export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) => {
|
||||
const { id, data } = el;
|
||||
const { fieldIdentifier, showDescription } = data;
|
||||
const description = useInputFieldUserDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const description = useInputFieldUserDescriptionSafe(fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateSafe(fieldIdentifier.fieldName);
|
||||
const containerCtx = useContainerContext();
|
||||
const formatFallbackLabel = useFormatFallbackLabel();
|
||||
|
||||
@@ -70,8 +70,8 @@ NodeFieldElementViewMode.displayName = 'NodeFieldElementViewMode';
|
||||
const NodeFieldElementViewModeContent = memo(({ el }: { el: NodeFieldElement }) => {
|
||||
const { data } = el;
|
||||
const { fieldIdentifier, showDescription } = data;
|
||||
const description = useInputFieldUserDescriptionSafe(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.nodeId, fieldIdentifier.fieldName);
|
||||
const description = useInputFieldUserDescriptionSafe(fieldIdentifier.fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldIdentifier.fieldName);
|
||||
|
||||
const _description = useMemo(
|
||||
() => description || fieldTemplate.description,
|
||||
|
||||
@@ -9,8 +9,8 @@ import { useCallback } from 'react';
|
||||
export const useAddNodeFieldToRoot = (nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const rootElementId = useAppSelector(selectFormRootElementId);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const field = useInputFieldInstance(nodeId, fieldName);
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(fieldName);
|
||||
const field = useInputFieldInstance(fieldName);
|
||||
|
||||
const addNodeFieldToRoot = useCallback(() => {
|
||||
const element = buildNodeFieldElement(nodeId, fieldName, fieldTemplate.type);
|
||||
|
||||
@@ -35,9 +35,9 @@ export default memo(InspectorDetailsTab);
|
||||
|
||||
const Content = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const version = useNodeVersion(nodeId);
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const needsUpdate = useNodeNeedsUpdate(nodeId);
|
||||
const version = useNodeVersion();
|
||||
const template = useNodeTemplateOrThrow();
|
||||
const needsUpdate = useNodeNeedsUpdate();
|
||||
|
||||
return (
|
||||
<Box position="relative" w="full" h="full">
|
||||
|
||||
@@ -37,7 +37,7 @@ const getKey = (result: AnyInvocationOutput, i: number) => `${result.type}-${i}`
|
||||
|
||||
const Content = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const template = useNodeTemplateOrThrow();
|
||||
const nes = useNodeExecutionState(nodeId);
|
||||
|
||||
if (!nes || nes.outputs.length === 0) {
|
||||
|
||||
@@ -14,8 +14,8 @@ type Props = {
|
||||
|
||||
const InspectorTabEditableNodeTitle = ({ nodeId, title }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const label = useNodeUserTitleSafe(nodeId);
|
||||
const templateTitle = useNodeTemplateTitleSafe(nodeId);
|
||||
const label = useNodeUserTitleSafe();
|
||||
const templateTitle = useNodeTemplateTitleSafe();
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const onChange = useCallback(
|
||||
|
||||
@@ -29,7 +29,7 @@ export default memo(NodeTemplateInspector);
|
||||
|
||||
const Content = memo(({ nodeId }: { nodeId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const template = useNodeTemplateOrThrow();
|
||||
|
||||
return <DataViewer data={template} label={t('nodes.nodeTemplate')} bg="base.850" color="base.200" />;
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@ import { memo } from 'react';
|
||||
// easier to handle cases where we are missing a node template in the inspector.
|
||||
|
||||
export const TemplateGate = memo(
|
||||
({ nodeId, fallback, children }: PropsWithChildren<{ nodeId: string; fallback: ReactNode }>) => {
|
||||
const template = useNodeTemplateSafe(nodeId);
|
||||
({ fallback, children }: PropsWithChildren<{ nodeId: string; fallback: ReactNode }>) => {
|
||||
const template = useNodeTemplateSafe();
|
||||
|
||||
if (!template) {
|
||||
return fallback;
|
||||
|
||||
@@ -96,7 +96,7 @@ const OutputFields = memo(() => {
|
||||
OutputFields.displayName = 'OutputFields';
|
||||
|
||||
const OutputFieldsContent = memo(({ outputNodeId }: { outputNodeId: string }) => {
|
||||
const outputFieldNames = useOutputFieldNames(outputNodeId);
|
||||
const outputFieldNames = useOutputFieldNames();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -291,10 +291,10 @@ PublishWorkflowButton.displayName = 'DoValidationRunButton';
|
||||
|
||||
const NodeInputFieldPreview = memo(({ nodeId, fieldName }: { nodeId: string; fieldName: string }) => {
|
||||
const mouseOverFormField = useMouseOverFormField(nodeId);
|
||||
const nodeUserTitle = useNodeUserTitleOrThrow(nodeId);
|
||||
const nodeTemplateTitle = useNodeTemplateTitleOrThrow(nodeId);
|
||||
const fieldUserTitle = useInputFieldUserTitleOrThrow(nodeId, fieldName);
|
||||
const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(nodeId, fieldName);
|
||||
const nodeUserTitle = useNodeUserTitleOrThrow();
|
||||
const nodeTemplateTitle = useNodeTemplateTitleOrThrow();
|
||||
const fieldUserTitle = useInputFieldUserTitleOrThrow(fieldName);
|
||||
const fieldTemplateTitle = useInputFieldTemplateTitleOrThrow(fieldName);
|
||||
const zoomToNode = useZoomToNode(nodeId);
|
||||
|
||||
return (
|
||||
@@ -317,9 +317,9 @@ NodeInputFieldPreview.displayName = 'NodeInputFieldPreview';
|
||||
|
||||
const NodeOutputFieldPreview = memo(({ nodeId, fieldName }: { nodeId: string; fieldName: string }) => {
|
||||
const mouseOverFormField = useMouseOverFormField(nodeId);
|
||||
const nodeUserTitle = useNodeUserTitleOrThrow(nodeId);
|
||||
const nodeTemplateTitle = useNodeTemplateTitleOrThrow(nodeId);
|
||||
const fieldTemplate = useOutputFieldTemplate(nodeId, fieldName);
|
||||
const nodeUserTitle = useNodeUserTitleOrThrow();
|
||||
const nodeTemplateTitle = useNodeTemplateTitleOrThrow();
|
||||
const fieldTemplate = useOutputFieldTemplate(fieldName);
|
||||
const zoomToNode = useZoomToNode(nodeId);
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectNodes } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
|
||||
import { useMemo } from 'react';
|
||||
@@ -11,8 +11,11 @@ export const useGetNodesNeedUpdate = () => {
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) =>
|
||||
nodes.nodes.filter(isInvocationNode).some((node) => {
|
||||
createSelector(selectNodes, (nodes) =>
|
||||
nodes.some((node) => {
|
||||
if (!isInvocationNode(node)) {
|
||||
return false; // Invocation nodes do not need updates
|
||||
}
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
return false;
|
||||
|
||||
@@ -1,33 +1,34 @@
|
||||
import { objectEquals } from '@observ33r/object-equals';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInputFieldTemplateOrThrow } from 'features/nodes/hooks/useInputFieldTemplateOrThrow';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const useInputFieldDefaultValue = (nodeId: string, fieldName: string) => {
|
||||
export const useInputFieldDefaultValue = (fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selectDefaultValue = useMemo(
|
||||
() => createSelector(ctx.buildSelectInputFieldTemplateOrThrow(fieldName), (fieldTemplate) => fieldTemplate.default),
|
||||
[ctx, fieldName]
|
||||
);
|
||||
const defaultValue = useAppSelector(selectDefaultValue);
|
||||
|
||||
const fieldTemplate = useInputFieldTemplateOrThrow(nodeId, fieldName);
|
||||
const selectIsChanged = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const node = nodes.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return;
|
||||
createSelector(
|
||||
[ctx.buildSelectInputFieldOrThrow(fieldName), selectDefaultValue],
|
||||
(fieldInstance, defaultValue) => {
|
||||
return !objectEquals(fieldInstance.value, defaultValue);
|
||||
}
|
||||
const value = node.data.inputs[fieldName]?.value;
|
||||
return !objectEquals(value, fieldTemplate.default);
|
||||
}),
|
||||
[fieldName, fieldTemplate.default, nodeId]
|
||||
),
|
||||
[fieldName, selectDefaultValue, ctx]
|
||||
);
|
||||
const isValueChanged = useAppSelector(selectIsChanged);
|
||||
|
||||
const resetToDefaultValue = useCallback(() => {
|
||||
dispatch(fieldValueReset({ nodeId, fieldName, value: fieldTemplate.default }));
|
||||
}, [dispatch, fieldName, fieldTemplate.default, nodeId]);
|
||||
dispatch(fieldValueReset({ nodeId: ctx.nodeId, fieldName, value: defaultValue }));
|
||||
}, [dispatch, fieldName, defaultValue, ctx.nodeId]);
|
||||
|
||||
return { defaultValue: fieldTemplate.default, isValueChanged, resetToDefaultValue };
|
||||
return { defaultValue, isValueChanged, resetToDefaultValue };
|
||||
};
|
||||
|
||||
@@ -1,44 +1,36 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useDebouncedAppSelector } from 'app/store/use-debounced-app-selector';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectFieldInputInstance, selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { getFieldErrors } from 'features/nodes/store/util/fieldValidators';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { FieldError } from 'features/nodes/store/util/fieldValidators';
|
||||
import { $nodeErrors } from 'features/nodes/store/util/fieldValidators';
|
||||
import { computed } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
* A hook that returns the errors for a given input field. The errors calculation is debounced.
|
||||
*
|
||||
* @param nodeId The id of the node
|
||||
* @param fieldName The name of the field
|
||||
* @returns An array of FieldError objects
|
||||
*/
|
||||
export const useInputFieldErrors = (nodeId: string, fieldName: string) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
const selectFieldErrors = useMemo(
|
||||
export const useInputFieldErrors = (fieldName: string): FieldError[] => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const $errors = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const node = selectInvocationNodeSafe(nodes, nodeId);
|
||||
if (!node) {
|
||||
// If the node is not found, return an empty array - might happen during node deletion
|
||||
return [];
|
||||
computed($nodeErrors, (nodeErrors) => {
|
||||
const thisNodeErrors = nodeErrors[ctx.nodeId];
|
||||
if (!thisNodeErrors) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
const field = selectFieldInputInstance(nodes, nodeId, fieldName);
|
||||
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
assert(nodeTemplate, `Template for input node type ${node.data.type} not found.`);
|
||||
|
||||
const fieldTemplate = nodeTemplate.inputs[fieldName];
|
||||
assert(fieldTemplate, `Template for input field ${fieldName} not found.`);
|
||||
|
||||
return getFieldErrors(node, nodeTemplate, field, fieldTemplate, nodes);
|
||||
const errors = thisNodeErrors.filter((error) => {
|
||||
error.type === 'field-error' && error.fieldName === fieldName;
|
||||
});
|
||||
if (errors.length === 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
return errors as FieldError[];
|
||||
}),
|
||||
[nodeId, fieldName, templates]
|
||||
[ctx, fieldName]
|
||||
);
|
||||
|
||||
const fieldErrors = useDebouncedAppSelector(selectFieldErrors);
|
||||
|
||||
return fieldErrors;
|
||||
return useStore($errors);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { FieldInputInstance } from 'features/nodes/types/field';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useInputFieldInstance = <T extends FieldInputInstance>(nodeId: string, fieldName: string): T => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const instance = selectFieldInputInstanceSafe(nodes, nodeId, fieldName);
|
||||
assert(instance, `Instance for input field ${fieldName} not found`);
|
||||
return instance;
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
|
||||
const instance = useAppSelector(selector);
|
||||
|
||||
return instance as T;
|
||||
export const useInputFieldInstance = <T extends FieldInputInstance>(fieldName: string): T => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => {
|
||||
return ctx.buildSelectInputFieldOrThrow(fieldName);
|
||||
}, [ctx, fieldName]);
|
||||
return useAppSelector(selector) as T;
|
||||
};
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useInputFieldInstanceExists = (nodeId: string, fieldName: string) => {
|
||||
export const useInputFieldInstanceExists = (fieldName: string): boolean => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNodeSafe(nodesSlice, nodeId);
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const instance = node.data.inputs[fieldName];
|
||||
return Boolean(instance);
|
||||
createSelector(ctx.buildSelectInputFieldSafe(fieldName), (field) => {
|
||||
return !!field;
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
[ctx, fieldName]
|
||||
);
|
||||
|
||||
const exists = useAppSelector(selector);
|
||||
|
||||
return exists;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useInputFieldIsConnected = (nodeId: string, fieldName: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const firstConnectedEdge = nodes.edges.find((edge) => {
|
||||
return edge.target === nodeId && edge.targetHandle === fieldName;
|
||||
});
|
||||
return firstConnectedEdge !== undefined;
|
||||
}),
|
||||
[fieldName, nodeId]
|
||||
);
|
||||
export const useInputFieldIsConnected = (fieldName: string) => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => ctx.buildSelectIsInputFieldConnected(fieldName), [fieldName, ctx]);
|
||||
|
||||
const isConnected = useAppSelector(selector);
|
||||
|
||||
return isConnected;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,46 +1,32 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useDebouncedAppSelector } from 'app/store/use-debounced-app-selector';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectFieldInputInstance, selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { getFieldErrors } from 'features/nodes/store/util/fieldValidators';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { $nodeErrors } from 'features/nodes/store/util/fieldValidators';
|
||||
import { computed } from 'nanostores';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
/**
|
||||
* A hook that returns a boolean representing whether the field is invalid. A field is invalid if it has any errors.
|
||||
* The errors calculation is debounced.
|
||||
*
|
||||
* @param nodeId The id of the node
|
||||
* @param fieldName The name of the field
|
||||
*
|
||||
* @returns A boolean representing whether the field is invalid
|
||||
*/
|
||||
export const useInputFieldIsInvalid = (nodeId: string, fieldName: string) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
const selectIsInvalid = useMemo(
|
||||
export const useInputFieldIsInvalid = (fieldName: string) => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const $isInvalid = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const node = selectInvocationNodeSafe(nodes, nodeId);
|
||||
if (!node) {
|
||||
// If the node is not found, return false - might happen during node deletion
|
||||
computed($nodeErrors, (nodeErrors) => {
|
||||
const thisNodeErrors = nodeErrors[ctx.nodeId];
|
||||
if (!thisNodeErrors) {
|
||||
return false;
|
||||
}
|
||||
const field = selectFieldInputInstance(nodes, nodeId, fieldName);
|
||||
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
assert(nodeTemplate, `Template for input node type ${node.data.type} not found.`);
|
||||
|
||||
const fieldTemplate = nodeTemplate.inputs[fieldName];
|
||||
assert(fieldTemplate, `Template for input field ${fieldName} not found.`);
|
||||
|
||||
return getFieldErrors(node, nodeTemplate, field, fieldTemplate, nodes).length > 0;
|
||||
const isFieldInvalid = thisNodeErrors.some((error) => {
|
||||
error.type === 'field-error' && error.fieldName === fieldName;
|
||||
});
|
||||
return isFieldInvalid;
|
||||
}),
|
||||
[nodeId, fieldName, templates]
|
||||
[ctx, fieldName]
|
||||
);
|
||||
|
||||
const isInvalid = useDebouncedAppSelector(selectIsInvalid);
|
||||
|
||||
return isInvalid;
|
||||
return useStore($isInvalid);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useInputFieldNameSafe = (nodeId: string, fieldName: string) => {
|
||||
const templates = useStore($templates);
|
||||
export const useInputFieldNameSafe = (fieldName: string) => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNodeSafe(nodesSlice, nodeId);
|
||||
if (!node) {
|
||||
return fieldName;
|
||||
createSelector(
|
||||
[ctx.buildSelectInputFieldSafe(fieldName), ctx.buildSelectInputFieldTemplateSafe(fieldName)],
|
||||
(fieldInstance, fieldTemplate) => {
|
||||
const name = fieldInstance?.label || fieldTemplate?.title || fieldName;
|
||||
return name;
|
||||
}
|
||||
const instance = node.data.inputs[fieldName];
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
const fieldTemplate = nodeTemplate?.inputs[fieldName];
|
||||
const name = instance?.label || fieldTemplate?.title || fieldName;
|
||||
return name;
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
),
|
||||
[fieldName, ctx]
|
||||
);
|
||||
|
||||
const name = useAppSelector(selector);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useNodeData } from 'features/nodes/hooks/useNodeData';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { isSingleOrCollection } from 'features/nodes/types/field';
|
||||
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
|
||||
const isConnectionInputField = (field: FieldInputTemplate) => {
|
||||
return (
|
||||
(field.input === 'connection' && !isSingleOrCollection(field.type)) || !(field.type.name in TEMPLATE_BUILDER_MAP)
|
||||
@@ -19,41 +19,52 @@ const isAnyOrDirectInputField = (field: FieldInputTemplate) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const useInputFieldNamesMissing = (nodeId: string) => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const node = useNodeData(nodeId);
|
||||
const fieldNames = useMemo(() => {
|
||||
const instanceFields = new Set(Object.keys(node.inputs));
|
||||
const allTemplateFields = new Set(Object.keys(template.inputs));
|
||||
return Array.from(instanceFields.difference(allTemplateFields));
|
||||
}, [node.inputs, template.inputs]);
|
||||
return fieldNames;
|
||||
export const useInputFieldNamesMissing = () => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector([ctx.selectNodeInputsOrThrow, ctx.selectNodeTemplateSafe], (inputs, template) => {
|
||||
const instanceFieldNames = new Set(Object.keys(inputs));
|
||||
const templateFieldNames = new Set(Object.keys(template?.inputs ?? {}));
|
||||
return Array.from(instanceFieldNames.difference(templateFieldNames));
|
||||
}),
|
||||
[ctx]
|
||||
);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
export const useInputFieldNamesAnyOrDirect = (nodeId: string) => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const fieldNames = useMemo(() => {
|
||||
const anyOrDirectFields: string[] = [];
|
||||
for (const [fieldName, fieldTemplate] of Object.entries(template.inputs)) {
|
||||
if (isAnyOrDirectInputField(fieldTemplate)) {
|
||||
anyOrDirectFields.push(fieldName);
|
||||
}
|
||||
}
|
||||
return anyOrDirectFields;
|
||||
}, [template.inputs]);
|
||||
return fieldNames;
|
||||
export const useInputFieldNamesAnyOrDirect = () => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector([ctx.selectNodeTemplateSafe], (template) => {
|
||||
const fieldNames: string[] = [];
|
||||
for (const [fieldName, fieldTemplate] of Object.entries(template?.inputs ?? {})) {
|
||||
if (isAnyOrDirectInputField(fieldTemplate)) {
|
||||
fieldNames.push(fieldName);
|
||||
}
|
||||
}
|
||||
return fieldNames;
|
||||
}),
|
||||
[ctx]
|
||||
);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
export const useInputFieldNamesConnection = (nodeId: string) => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const fieldNames = useMemo(() => {
|
||||
const connectionFields: string[] = [];
|
||||
for (const [fieldName, fieldTemplate] of Object.entries(template.inputs)) {
|
||||
if (isConnectionInputField(fieldTemplate)) {
|
||||
connectionFields.push(fieldName);
|
||||
}
|
||||
}
|
||||
return connectionFields;
|
||||
}, [template.inputs]);
|
||||
return fieldNames;
|
||||
export const useInputFieldNamesConnection = () => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector([ctx.selectNodeTemplateSafe], (template) => {
|
||||
const fieldNames: string[] = [];
|
||||
for (const [fieldName, fieldTemplate] of Object.entries(template?.inputs ?? {})) {
|
||||
if (isConnectionInputField(fieldTemplate)) {
|
||||
fieldNames.push(fieldName);
|
||||
}
|
||||
}
|
||||
return fieldNames;
|
||||
}),
|
||||
[ctx]
|
||||
);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useInputFieldTemplateExists = (nodeId: string, fieldName: string) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
export const useInputFieldTemplateExists = (fieldName: string): boolean => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNodeSafe(nodesSlice, nodeId);
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
const fieldTemplate = nodeTemplate?.inputs[fieldName];
|
||||
return Boolean(fieldTemplate);
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
() => createSelector(ctx.buildSelectInputFieldTemplateSafe(fieldName), (fieldTemplate) => !!fieldTemplate),
|
||||
[ctx, fieldName]
|
||||
);
|
||||
|
||||
const exists = useAppSelector(selector);
|
||||
|
||||
return exists;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
|
||||
/**
|
||||
* Returns the template for a specific input field of a node.
|
||||
*
|
||||
* **Note:** This hook will throw an error if the template for the input field is not found.
|
||||
*
|
||||
* @param nodeId - The ID of the node.
|
||||
* @param fieldName - The name of the input field.
|
||||
* @throws Will throw an error if the template for the input field is not found.
|
||||
*/
|
||||
export const useInputFieldTemplateOrThrow = (nodeId: string, fieldName: string): FieldInputTemplate => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const fieldTemplate = useMemo(() => {
|
||||
const _fieldTemplate = template.inputs[fieldName];
|
||||
assert(_fieldTemplate, `Template for input field ${fieldName} not found.`);
|
||||
return _fieldTemplate;
|
||||
}, [fieldName, template.inputs]);
|
||||
return fieldTemplate;
|
||||
export const useInputFieldTemplateOrThrow = (fieldName: string): FieldInputTemplate => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => ctx.buildSelectInputFieldTemplateOrThrow(fieldName), [ctx, fieldName]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNodeTemplateSafe } from 'features/nodes/hooks/useNodeTemplateSafe';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { FieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
@@ -10,8 +11,8 @@ import { useMemo } from 'react';
|
||||
* @param nodeId - The ID of the node.
|
||||
* @param fieldName - The name of the input field.
|
||||
*/
|
||||
export const useInputFieldTemplateSafe = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
|
||||
const template = useNodeTemplateSafe(nodeId);
|
||||
const fieldTemplate = useMemo(() => template?.inputs[fieldName] ?? null, [fieldName, template?.inputs]);
|
||||
return fieldTemplate;
|
||||
export const useInputFieldTemplateSafe = (fieldName: string): FieldInputTemplate | null => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => ctx.buildSelectInputFieldTemplateSafe(fieldName), [ctx, fieldName]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
|
||||
export const useInputFieldTemplateTitleOrThrow = (nodeId: string, fieldName: string): string => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
|
||||
const title = useMemo(() => {
|
||||
const fieldTemplate = template.inputs[fieldName];
|
||||
assert(fieldTemplate, `Template for input field ${fieldName} not found.`);
|
||||
return fieldTemplate.title;
|
||||
}, [fieldName, template.inputs]);
|
||||
|
||||
return title;
|
||||
export const useInputFieldTemplateTitleOrThrow = (fieldName: string): string => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() => createSelector(ctx.buildSelectInputFieldTemplateOrThrow(fieldName), (fieldTemplate) => fieldTemplate.title),
|
||||
[ctx, fieldName]
|
||||
);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
@@ -8,19 +8,13 @@ import { useMemo } from 'react';
|
||||
*
|
||||
* If the node doesn't exist or is not an invocation node, an empty string is returned.
|
||||
*
|
||||
* @param nodeId The ID of the node
|
||||
* @param fieldName The name of the field
|
||||
*/
|
||||
export const useInputFieldUserDescriptionSafe = (nodeId: string, fieldName: string) => {
|
||||
export const useInputFieldUserDescriptionSafe = (fieldName: string) => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(
|
||||
selectNodesSlice,
|
||||
(nodes) => selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.description ?? ''
|
||||
),
|
||||
[fieldName, nodeId]
|
||||
() => createSelector(ctx.buildSelectInputFieldSafe(fieldName), (field) => field?.description ?? ''),
|
||||
[ctx, fieldName]
|
||||
);
|
||||
|
||||
const description = useAppSelector(selector);
|
||||
return description;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
@@ -8,16 +8,13 @@ import { useMemo } from 'react';
|
||||
*
|
||||
* If the node doesn't exist or is not an invocation node, an error is thrown.
|
||||
*
|
||||
* @param nodeId The ID of the node
|
||||
* @param fieldName The name of the field
|
||||
*/
|
||||
export const useInputFieldUserTitleOrThrow = (nodeId: string, fieldName: string): string => {
|
||||
export const useInputFieldUserTitleOrThrow = (fieldName: string): string => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() => createSelector(selectNodesSlice, (nodes) => selectFieldInputInstance(nodes, nodeId, fieldName).label),
|
||||
[fieldName, nodeId]
|
||||
() => createSelector(ctx.buildSelectInputFieldOrThrow(fieldName), (field) => field.label),
|
||||
[ctx, fieldName]
|
||||
);
|
||||
|
||||
const title = useAppSelector(selector);
|
||||
|
||||
return title;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectFieldInputInstanceSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
/**
|
||||
@@ -8,17 +8,13 @@ import { useMemo } from 'react';
|
||||
*
|
||||
* If the node doesn't exist or is not an invocation node, an empty string is returned.
|
||||
*
|
||||
* @param nodeId The ID of the node
|
||||
* @param fieldName The name of the field
|
||||
*/
|
||||
export const useInputFieldUserTitleSafe = (nodeId: string, fieldName: string): string => {
|
||||
export const useInputFieldUserTitleSafe = (fieldName: string): string => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => selectFieldInputInstanceSafe(nodes, nodeId, fieldName)?.label ?? ''),
|
||||
[fieldName, nodeId]
|
||||
() => createSelector(ctx.buildSelectInputFieldSafe(fieldName), (field) => field?.label ?? ''),
|
||||
[ctx, fieldName]
|
||||
);
|
||||
|
||||
const title = useAppSelector(selector);
|
||||
|
||||
return title;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useMemo } from 'react';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
|
||||
export const useIsExecutableNode = (nodeId: string) => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
export const useIsExecutableNode = () => {
|
||||
const template = useNodeTemplateOrThrow();
|
||||
const isExecutableNode = useMemo(
|
||||
() => !isBatchNodeType(template.type) && !isGeneratorNodeType(template.type),
|
||||
[template]
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useMemo } from 'react';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
|
||||
export const useNodeClassification = (nodeId: string): Classification => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
export const useNodeClassification = (): Classification => {
|
||||
const template = useNodeTemplateOrThrow();
|
||||
const classification = useMemo(() => template.classification, [template]);
|
||||
return classification;
|
||||
};
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { InvocationNodeData } from 'features/nodes/types/invocation';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNodeData = (nodeId: string): InvocationNodeData => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
return selectNodeData(nodes, nodeId);
|
||||
}),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const nodeData = useAppSelector(selector);
|
||||
|
||||
return nodeData;
|
||||
export const useNodeData = (): InvocationNodeData => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
return useAppSelector(ctx.selectNodeDataOrThrow);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { selectNodes } from 'features/nodes/store/selectors';
|
||||
import type { NodeExecutionStates } from 'features/nodes/store/types';
|
||||
import type { NodeExecutionState } from 'features/nodes/types/invocation';
|
||||
import { zNodeStatus } from 'features/nodes/types/invocation';
|
||||
@@ -38,7 +38,7 @@ export const upsertExecutionState = (nodeId: string, updates?: Partial<NodeExecu
|
||||
}
|
||||
};
|
||||
|
||||
const selectNodeIds = createMemoizedSelector(selectNodesSlice, (nodesSlice) => nodesSlice.nodes.map((node) => node.id));
|
||||
const selectNodeIds = createMemoizedSelector(selectNodes, (nodes) => nodes.map((node) => node.id));
|
||||
|
||||
export const useSyncExecutionState = () => {
|
||||
const nodeIds = useAppSelector(selectNodeIds);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { some } from 'es-toolkit/compat';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
import { useNodeTemplateSafe } from './useNodeTemplateSafe';
|
||||
|
||||
export const useNodeHasImageOutput = (nodeId: string): boolean => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
export const useNodeHasImageOutput = (): boolean => {
|
||||
const template = useNodeTemplateSafe();
|
||||
const hasImageOutput = useMemo(
|
||||
() =>
|
||||
some(
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNodeIsIntermediate = (nodeId: string): boolean => {
|
||||
export const useNodeIsIntermediate = (): boolean => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
return selectNodeData(nodes, nodeId)?.isIntermediate ?? false;
|
||||
createSelector(ctx.selectNodeDataSafe, (data) => {
|
||||
return data?.isIntermediate ?? false;
|
||||
}),
|
||||
[nodeId]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const isIntermediate = useAppSelector(selector);
|
||||
return isIntermediate;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { getInvocationNodeErrors } from 'features/nodes/store/util/fieldValidators';
|
||||
import { useMemo } from 'react';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { $nodeErrors } from 'features/nodes/store/util/fieldValidators';
|
||||
|
||||
export const useNodeIsInvalid = (nodeId: string) => {
|
||||
const templates = useStore($templates);
|
||||
const selectHasErrors = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const errors = getInvocationNodeErrors(nodeId, templates, nodes);
|
||||
return errors.length > 0;
|
||||
}),
|
||||
[nodeId, templates]
|
||||
);
|
||||
const hasErrors = useAppSelector(selectHasErrors);
|
||||
export const useNodeIsInvalid = () => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const hasErrors = useStore($nodeErrors, { keys: [ctx.nodeId] })[ctx.nodeId];
|
||||
return hasErrors;
|
||||
};
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useMemo } from 'react';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
|
||||
export const useNodeNeedsUpdate = (nodeId: string) => {
|
||||
const type = useNodeType(nodeId);
|
||||
const version = useNodeVersion(nodeId);
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
export const useNodeNeedsUpdate = () => {
|
||||
const type = useNodeType();
|
||||
const version = useNodeVersion();
|
||||
const template = useNodeTemplateOrThrow();
|
||||
const needsUpdate = useMemo(() => {
|
||||
if (type !== template.type) {
|
||||
return true;
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useInvocationNodeNotes = (nodeId: string): string => {
|
||||
export const useInvocationNodeNotes = (): string => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
const node = selectInvocationNode(nodes, nodeId);
|
||||
return node.data.notes;
|
||||
createSelector(ctx.selectNodeDataSafe, (data) => {
|
||||
return data?.notes ?? '';
|
||||
}),
|
||||
[nodeId]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const notes = useAppSelector(selector);
|
||||
|
||||
return notes;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNodePack = (nodeId: string): string | null => {
|
||||
export const useNodePack = (): string | null => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
return selectNodeData(nodes, nodeId)?.nodePack ?? null;
|
||||
createSelector(ctx.selectNodeDataSafe, (data) => {
|
||||
return data?.nodePack ?? null;
|
||||
}),
|
||||
[nodeId]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const nodePack = useAppSelector(selector);
|
||||
return nodePack;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useNodeType } from 'features/nodes/hooks/useNodeType';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { InvocationTemplate } from 'features/nodes/types/invocation';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useNodeTemplateOrThrow = (nodeId: string): InvocationTemplate => {
|
||||
const templates = useStore($templates);
|
||||
const type = useNodeType(nodeId);
|
||||
const template = useMemo(() => {
|
||||
const t = templates[type];
|
||||
assert(t, `Template for node type ${type} not found`);
|
||||
return t;
|
||||
}, [templates, type]);
|
||||
return template;
|
||||
export const useNodeTemplateOrThrow = (): InvocationTemplate => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
return useAppSelector(ctx.selectNodeTemplateOrThrow);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useNodeType } from 'features/nodes/hooks/useNodeType';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { InvocationTemplate } from 'features/nodes/types/invocation';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNodeTemplateSafe = (nodeId: string): InvocationTemplate | null => {
|
||||
const templates = useStore($templates);
|
||||
const type = useNodeType(nodeId);
|
||||
const template = useMemo(() => templates[type] ?? null, [templates, type]);
|
||||
return template;
|
||||
export const useNodeTemplateSafe = (): InvocationTemplate | null => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
return useAppSelector(ctx.selectNodeTemplateSafe);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,10 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useNodeTemplateTitleOrThrow = (nodeId: string): string => {
|
||||
const templates = useStore($templates);
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
||||
assert(isInvocationNode(node), 'Node not found');
|
||||
const template = templates[node.data.type];
|
||||
assert(template, 'Template not found');
|
||||
return template.title;
|
||||
}),
|
||||
[nodeId, templates]
|
||||
);
|
||||
const title = useAppSelector(selector);
|
||||
return title;
|
||||
export const useNodeTemplateTitleOrThrow = (): string => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => createSelector(ctx.selectNodeTemplateOrThrow, (template) => template.title), [ctx]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNodeTemplateTitleSafe = (nodeId: string): string | null => {
|
||||
const templates = useStore($templates);
|
||||
export const useNodeTemplateTitleSafe = (): string | null => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
||||
if (!isInvocationNode(node)) {
|
||||
return null;
|
||||
}
|
||||
const template = templates[node.data.type];
|
||||
return template?.title ?? null;
|
||||
}),
|
||||
[nodeId, templates]
|
||||
() => createSelector(ctx.selectNodeTemplateSafe, (template) => template?.title ?? ''),
|
||||
[ctx]
|
||||
);
|
||||
const title = useAppSelector(selector);
|
||||
return title;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useMemo } from 'react';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
|
||||
export const useNodeType = (nodeId: string): string => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
return selectNodeData(nodes, nodeId).type;
|
||||
}),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const type = useAppSelector(selector);
|
||||
|
||||
return type;
|
||||
export const useNodeType = (): string => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
return useAppSelector(ctx.selectNodeTypeOrThrow);
|
||||
};
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
export const useNodeUserTitleOrThrow = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
||||
assert(isInvocationNode(node), 'Node not found');
|
||||
return node.data.label;
|
||||
}),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const title = useAppSelector(selector);
|
||||
return title;
|
||||
export const useNodeUserTitleOrThrow = () => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => createSelector(ctx.selectNodeDataOrThrow, (data) => data.label), [ctx]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNodeUserTitleSafe = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = nodesSlice.nodes.find((node) => node.id === nodeId);
|
||||
return node?.data.label ?? null;
|
||||
}),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const title = useAppSelector(selector);
|
||||
return title;
|
||||
export const useNodeUserTitleSafe = () => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => createSelector(ctx.selectNodeDataSafe, (data) => data?.label ?? ''), [ctx]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useNodeVersion = (nodeId: string) => {
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
return node.data.version;
|
||||
}),
|
||||
[nodeId]
|
||||
);
|
||||
|
||||
const version = useAppSelector(selector);
|
||||
return version;
|
||||
export const useNodeVersion = () => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => createSelector(ctx.selectNodeDataOrThrow, (data) => data.version), [ctx]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useOutputFieldName = (nodeId: string, fieldName: string) => {
|
||||
const templates = useStore($templates);
|
||||
export const useOutputFieldName = (fieldName: string) => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNode(nodesSlice, nodeId);
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
const fieldTemplate = nodeTemplate?.outputs[fieldName];
|
||||
createSelector([ctx.buildSelectOutputFieldTemplateSafe(fieldName)], (fieldTemplate) => {
|
||||
const name = fieldTemplate?.title || fieldName;
|
||||
return name;
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
[fieldName, ctx]
|
||||
);
|
||||
|
||||
const name = useAppSelector(selector);
|
||||
|
||||
return name;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { map } from 'es-toolkit/compat';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
|
||||
export const useOutputFieldNames = (nodeId: string): string[] => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const fieldNames = useMemo(() => getSortedFilteredFieldNames(map(template.outputs)), [template.outputs]);
|
||||
return fieldNames;
|
||||
export const useOutputFieldNames = (): string[] => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector([ctx.selectNodeTemplateOrThrow], (template) =>
|
||||
getSortedFilteredFieldNames(Object.values(template.outputs))
|
||||
),
|
||||
[ctx]
|
||||
);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import type { FieldOutputTemplate } from 'features/nodes/types/field';
|
||||
import { useMemo } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
import { useNodeTemplateOrThrow } from './useNodeTemplateOrThrow';
|
||||
|
||||
export const useOutputFieldTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate => {
|
||||
const template = useNodeTemplateOrThrow(nodeId);
|
||||
const fieldTemplate = useMemo(() => {
|
||||
const _fieldTemplate = template.outputs[fieldName];
|
||||
assert(_fieldTemplate, `Template for output field ${fieldName} not found`);
|
||||
return _fieldTemplate;
|
||||
}, [fieldName, template.outputs]);
|
||||
return fieldTemplate;
|
||||
export const useOutputFieldTemplate = (fieldName: string): FieldOutputTemplate => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(() => ctx.buildSelectOutputFieldTemplateOrThrow(fieldName), [ctx, fieldName]);
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,16 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectInvocationNodeSafe, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useOutputFieldTemplateExists = (nodeId: string, fieldName: string) => {
|
||||
const templates = useStore($templates);
|
||||
|
||||
export const useOutputFieldTemplateExists = (fieldName: string) => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodesSlice) => {
|
||||
const node = selectInvocationNodeSafe(nodesSlice, nodeId);
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
const fieldTemplate = nodeTemplate?.outputs[fieldName];
|
||||
return Boolean(fieldTemplate);
|
||||
createSelector(ctx.buildSelectOutputFieldTemplateSafe(fieldName), (fieldTemplate) => {
|
||||
return !!fieldTemplate;
|
||||
}),
|
||||
[fieldName, nodeId, templates]
|
||||
[ctx, fieldName]
|
||||
);
|
||||
|
||||
const exists = useAppSelector(selector);
|
||||
|
||||
return exists;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { useInvocationNodeContext } from 'features/nodes/components/flow/nodes/Invocation/context';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useUseCache = (nodeId: string) => {
|
||||
export const useUseCache = () => {
|
||||
const ctx = useInvocationNodeContext();
|
||||
const selector = useMemo(
|
||||
() =>
|
||||
createSelector(selectNodesSlice, (nodes) => {
|
||||
return selectNodeData(nodes, nodeId)?.useCache ?? false;
|
||||
createSelector(ctx.selectNodeDataSafe, (data) => {
|
||||
return data?.useCache ?? false;
|
||||
}),
|
||||
[nodeId]
|
||||
[ctx]
|
||||
);
|
||||
|
||||
const useCache = useAppSelector(selector);
|
||||
return useCache;
|
||||
return useAppSelector(selector);
|
||||
};
|
||||
|
||||
@@ -4,9 +4,9 @@ import { useMemo } from 'react';
|
||||
|
||||
import { useNodeHasImageOutput } from './useNodeHasImageOutput';
|
||||
|
||||
export const useWithFooter = (nodeId: string) => {
|
||||
const hasImageOutput = useNodeHasImageOutput(nodeId);
|
||||
const isExecutableNode = useIsExecutableNode(nodeId);
|
||||
export const useWithFooter = () => {
|
||||
const hasImageOutput = useNodeHasImageOutput();
|
||||
const isExecutableNode = useIsExecutableNode();
|
||||
const isCacheEnabled = useFeatureStatus('invocationCache');
|
||||
const withFooter = useMemo(
|
||||
() => isExecutableNode && (hasImageOutput || isCacheEnabled),
|
||||
|
||||
@@ -223,63 +223,75 @@ export const nodesSlice = createSlice({
|
||||
//
|
||||
// But we don't have immer as an explicit dependency so we'll just cast.
|
||||
state.nodes = applyNodeChanges(action.payload, state.nodes) as typeof state.nodes;
|
||||
// Remove edges that are no longer valid, due to a removed or otherwise changed node
|
||||
const edgeChanges: EdgeChange<AnyEdge>[] = [];
|
||||
state.edges.forEach((e) => {
|
||||
const sourceExists = state.nodes.some((n) => n.id === e.source);
|
||||
const targetExists = state.nodes.some((n) => n.id === e.target);
|
||||
if (!(sourceExists && targetExists)) {
|
||||
edgeChanges.push({ type: 'remove', id: e.id });
|
||||
}
|
||||
});
|
||||
state.edges = applyEdgeChanges<AnyEdge>(edgeChanges, state.edges);
|
||||
|
||||
// If a node was removed, we should remove any form fields that were associated with it. However, node changes
|
||||
// may remove and then add the same node back. For example, when updating a workflow, we replace old nodes with
|
||||
// updated nodes. In this case, we should not remove the form fields. To handle this, we find the last remove
|
||||
// and add changes for each exposed field. If the remove change comes after the add change, we remove the exposed
|
||||
// field.
|
||||
for (const el of Object.values(state.form.elements)) {
|
||||
if (!isNodeFieldElement(el)) {
|
||||
continue;
|
||||
// Remove edges that are no longer valid, due to a removed or otherwise changed node
|
||||
const didNodesChange = action.payload.some(
|
||||
(change) => change.type === 'add' || change.type === 'remove' || change.type === 'replace'
|
||||
);
|
||||
|
||||
if (didNodesChange) {
|
||||
const edgeChanges: EdgeChange<AnyEdge>[] = [];
|
||||
for (const e of state.edges) {
|
||||
const sourceExists = state.nodes.some((n) => n.id === e.source);
|
||||
const targetExists = state.nodes.some((n) => n.id === e.target);
|
||||
if (!(sourceExists && targetExists)) {
|
||||
edgeChanges.push({ type: 'remove', id: e.id });
|
||||
}
|
||||
}
|
||||
const { nodeId } = el.data.fieldIdentifier;
|
||||
const removeIndex = action.payload.findLastIndex((change) => change.type === 'remove' && change.id === nodeId);
|
||||
const addIndex = action.payload.findLastIndex((change) => change.type === 'add' && change.item.id === nodeId);
|
||||
if (removeIndex > addIndex) {
|
||||
removeElement({ form: state.form, id: el.id });
|
||||
if (edgeChanges.length > 0) {
|
||||
state.edges = applyEdgeChanges<AnyEdge>(edgeChanges, state.edges);
|
||||
}
|
||||
}
|
||||
|
||||
const wereNodesRemoved = action.payload.some((change) => change.type === 'remove' || change.type === 'replace');
|
||||
|
||||
if (wereNodesRemoved) {
|
||||
// If a node was removed, we should remove any form fields that were associated with it. However, node changes
|
||||
// may remove and then add the same node back. For example, when updating a workflow, we replace old nodes with
|
||||
// updated nodes. In this case, we should not remove the form fields. To handle this, we find the last remove
|
||||
// and add changes for each exposed field. If the remove change comes after the add change, we remove the exposed
|
||||
// field.
|
||||
for (const el of Object.values(state.form.elements)) {
|
||||
if (!isNodeFieldElement(el)) {
|
||||
continue;
|
||||
}
|
||||
const { nodeId } = el.data.fieldIdentifier;
|
||||
const removeIndex = action.payload.findLastIndex(
|
||||
(change) => change.type === 'remove' && change.id === nodeId
|
||||
);
|
||||
const addIndex = action.payload.findLastIndex((change) => change.type === 'add' && change.item.id === nodeId);
|
||||
if (removeIndex > addIndex) {
|
||||
removeElement({ form: state.form, id: el.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
edgesChanged: (state, action: PayloadAction<EdgeChange<AnyEdge>[]>) => {
|
||||
const changes: EdgeChange<AnyEdge>[] = [];
|
||||
// We may need to massage the edge changes or otherwise handle them
|
||||
action.payload.forEach((change) => {
|
||||
for (const change of action.payload) {
|
||||
if (change.type === 'remove' || change.type === 'select') {
|
||||
const edge = state.edges.find((e) => e.id === change.id);
|
||||
// If we deleted or selected a collapsed edge, we need to find its "hidden" edges and do the same to them
|
||||
if (edge && edge.type === 'collapsed') {
|
||||
const hiddenEdges = state.edges.filter((e) => e.source === edge.source && e.target === edge.target);
|
||||
if (change.type === 'remove') {
|
||||
hiddenEdges.forEach(({ id }) => {
|
||||
for (const { id } of hiddenEdges) {
|
||||
if (change.type === 'remove') {
|
||||
changes.push({ type: 'remove', id });
|
||||
});
|
||||
}
|
||||
if (change.type === 'select') {
|
||||
hiddenEdges.forEach(({ id }) => {
|
||||
}
|
||||
if (change.type === 'select') {
|
||||
changes.push({ type: 'select', id, selected: change.selected });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (change.type === 'add') {
|
||||
} else if (change.type === 'add') {
|
||||
if (!change.item.type) {
|
||||
// We must add the edge type!
|
||||
change.item.type = 'default';
|
||||
}
|
||||
}
|
||||
changes.push(change);
|
||||
});
|
||||
}
|
||||
state.edges = applyEdgeChanges(changes, state.edges);
|
||||
},
|
||||
fieldLabelChanged: (
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { debounce } from 'es-toolkit';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { NodesState, Templates } from 'features/nodes/store/types';
|
||||
import type {
|
||||
FieldInputInstance,
|
||||
@@ -32,6 +38,8 @@ import {
|
||||
} from 'features/nodes/types/field';
|
||||
import { type InvocationNode, type InvocationTemplate, isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { t } from 'i18next';
|
||||
import { map } from 'nanostores';
|
||||
import { useEffect } from 'react';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
type FieldValidationFunc<TValue extends StatefulFieldValue, TTemplate extends FieldInputTemplate> = (
|
||||
@@ -164,13 +172,13 @@ const validateNumberFieldValue: FieldValidationFunc<
|
||||
return reasons;
|
||||
};
|
||||
|
||||
type NodeError = {
|
||||
export type NodeError = {
|
||||
type: 'node-error';
|
||||
nodeId: string;
|
||||
issue: string;
|
||||
};
|
||||
|
||||
type FieldError = {
|
||||
export type FieldError = {
|
||||
type: 'field-error';
|
||||
nodeId: string;
|
||||
fieldName: string;
|
||||
@@ -223,7 +231,7 @@ export const getFieldErrors = (
|
||||
nodeId,
|
||||
fieldName,
|
||||
prefix,
|
||||
issue: t('parameters.invoke.missingInputForField'),
|
||||
issue: 'parameters.invoke.missingInputForField',
|
||||
});
|
||||
} else if (isConnected) {
|
||||
// Connected fields have no value to validate - they are OK
|
||||
@@ -274,5 +282,57 @@ export const getInvocationNodeErrors = (
|
||||
errors.push(...getFieldErrors(node, nodeTemplate, field, fieldTemplate, nodesState));
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const $nodeErrors = map<Record<string, (NodeError | FieldError)[]>>({});
|
||||
|
||||
export const syncNodeErrors = (nodesState: NodesState, templates: Templates) => {
|
||||
for (const node of nodesState.nodes) {
|
||||
const errors: (NodeError | FieldError)[] = [];
|
||||
if (!isInvocationNode(node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nodeTemplate = templates[node.data.type];
|
||||
|
||||
if (!nodeTemplate) {
|
||||
errors.push({ type: 'node-error', nodeId: node.id, issue: t('parameters.invoke.missingNodeTemplate') });
|
||||
$nodeErrors.setKey(node.id, errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [fieldName, field] of Object.entries(node.data.inputs)) {
|
||||
const fieldTemplate = nodeTemplate.inputs[fieldName];
|
||||
|
||||
if (!fieldTemplate) {
|
||||
errors.push({ type: 'node-error', nodeId: node.id, issue: t('parameters.invoke.missingFieldTemplate') });
|
||||
$nodeErrors.setKey(node.id, errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
errors.push(...getFieldErrors(node, nodeTemplate, field, fieldTemplate, nodesState));
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
$nodeErrors.setKey(node.id, EMPTY_ARRAY);
|
||||
continue;
|
||||
}
|
||||
|
||||
$nodeErrors.setKey(node.id, errors);
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedSyncNodeErrors = debounce(syncNodeErrors, 300);
|
||||
|
||||
export const useSyncNodeErrors = () => {
|
||||
const nodesState = useAppSelector(selectNodesSlice);
|
||||
const templates = useStore($templates);
|
||||
useEffect(() => {
|
||||
debouncedSyncNodeErrors(nodesState, templates);
|
||||
}, [nodesState, templates]);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectUpscaleSlice } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
@@ -6,7 +6,7 @@ import { useMemo } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
const createIsTooLargeToUpscaleSelector = (imageDTO?: ImageDTO | null) =>
|
||||
createMemoizedSelector(selectUpscaleSlice, selectConfigSlice, (upscale, config) => {
|
||||
createSelector(selectUpscaleSlice, selectConfigSlice, (upscale, config) => {
|
||||
const { upscaleModel, scale } = upscale;
|
||||
const { maxUpscaleDimension } = config;
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import type { WorkflowV3 } from 'features/nodes/types/workflow';
|
||||
import { graphToWorkflow } from 'features/nodes/util/workflow/graphToWorkflow';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { useValidateAndLoadWorkflow } from 'features/workflowLibrary/hooks/useValidateAndLoadWorkflow';
|
||||
import { t } from 'i18next';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLazyGetImageWorkflowQuery } from 'services/api/endpoints/images';
|
||||
import type { NonNullableGraph } from 'services/api/types';
|
||||
import { assert } from 'tsafe';
|
||||
@@ -15,7 +15,6 @@ import { assert } from 'tsafe';
|
||||
* and handles the loading process.
|
||||
*/
|
||||
export const useLoadWorkflowFromImage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [getWorkflowAndGraphFromImage] = useLazyGetImageWorkflowQuery();
|
||||
const validateAndLoadWorkflow = useValidateAndLoadWorkflow();
|
||||
const loadWorkflowFromImage = useCallback(
|
||||
@@ -63,7 +62,7 @@ export const useLoadWorkflowFromImage = () => {
|
||||
onCompleted?.();
|
||||
}
|
||||
},
|
||||
[getWorkflowAndGraphFromImage, validateAndLoadWorkflow, t]
|
||||
[getWorkflowAndGraphFromImage, validateAndLoadWorkflow]
|
||||
);
|
||||
|
||||
return loadWorkflowFromImage;
|
||||
|
||||
@@ -11,8 +11,8 @@ import { validateWorkflow } from 'features/nodes/util/workflow/validateWorkflow'
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { VIEWER_PANEL_ID, WORKSPACE_PANEL_ID } from 'features/ui/layouts/shared';
|
||||
import { t } from 'i18next';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { serializeError } from 'serialize-error';
|
||||
import { checkBoardAccess, checkImageAccess, checkModelAccess } from 'services/api/hooks/accessChecks';
|
||||
import { z } from 'zod/v4';
|
||||
@@ -36,7 +36,6 @@ const log = logger('workflows');
|
||||
* ...each of which internally uses hook.
|
||||
*/
|
||||
export const useValidateAndLoadWorkflow = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const validateAndLoadWorkflow = useCallback(
|
||||
/**
|
||||
@@ -144,7 +143,7 @@ export const useValidateAndLoadWorkflow = () => {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[dispatch, t]
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return validateAndLoadWorkflow;
|
||||
|
||||
@@ -98,7 +98,10 @@ const dynamicBaseQuery: BaseQueryFn<string | FetchArgs, unknown, FetchBaseQueryE
|
||||
return rawBaseQuery(args, api, extraOptions);
|
||||
};
|
||||
|
||||
const createLruSelector = createSelectorCreator(lruMemoize);
|
||||
const createLruSelector = createSelectorCreator({
|
||||
memoize: lruMemoize,
|
||||
argsMemoize: lruMemoize,
|
||||
});
|
||||
|
||||
const customCreateApi = buildCreateApi(
|
||||
coreModule({ createSelector: createLruSelector }),
|
||||
|
||||
Reference in New Issue
Block a user