feat(ui): workflow schema v3 (WIP)

The changes aim to deduplicate data between workflows and node templates, decoupling workflows from internal implementation details. A good amount of data that was needlessly duplicated from the node template to the workflow is removed.

These changes substantially reduce the file size of workflows (and therefore the images with embedded workflows):

- Default T2I SD1.5 workflow JSON is reduced from 23.7kb (798 lines) to 10.9kb (407 lines).
- Default tiled upscale workflow JSON is reduced from 102.7kb (3341 lines) to 51.9kb (1774 lines).

The trade-off is that we need to reference node templates to get things like the field type and other things. In practice, this is a non-issue, because we need a node template to do anything with a node anyways.

- Field types are not included in the workflow. They are always pulled from the node templates.

The field type is now properly an internal implementation detail and we can change it as needed. Previously this would require a migration for the workflow itself. With the v3 schema, the structure of a field type is an internal implementation detail that we are free to change as we see fit.

- Workflow nodes no long have an `outputs` property and there is no longer such a thing as a `FieldOutputInstance`. These are only on the templates.

These were never referenced at a time when we didn't also have the templates available, and there'd be no reason to do so.

- Node width and height are no longer stored in the node.

These weren't used. Also, per https://reactflow.dev/api-reference/types/node, we shouldn't be programmatically changing these properties. A future enhancement can properly add node resizing.

- `nodeTemplates` slice is merged back into `nodesSlice` as `nodes.templates`. Turns out it's just a hassle having these separate in separate slices.

- Workflow migration logic updated to support the new schema. V1 workflows migrate all the way to v3 now.

- Changes throughout the nodes code to accommodate the above changes.
This commit is contained in:
psychedelicious
2024-02-13 16:30:00 +11:00
parent 5fbfed30ac
commit f8525837b2
80 changed files with 1940 additions and 616 deletions

View File

@ -1,26 +1,22 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { EMPTY_ARRAY } from 'app/store/util';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeTemplate } from 'features/nodes/store/selectors';
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
import { keys, map } from 'lodash-es';
import { useMemo } from 'react';
export const useAnyOrDirectInputFieldNames = (nodeId: string) => {
export const useAnyOrDirectInputFieldNames = (nodeId: string): string[] => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return [];
createMemoizedSelector(selectNodesSlice, (nodes) => {
const template = selectNodeTemplate(nodes, nodeId);
if (!template) {
return EMPTY_ARRAY;
}
const nodeTemplate = nodeTemplates.templates[node.data.type];
if (!nodeTemplate) {
return [];
}
const fields = map(nodeTemplate.inputs).filter(
const fields = map(template.inputs).filter(
(field) =>
(['any', 'direct'].includes(field.input) || field.type.isCollectionOrScalar) &&
keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)

View File

@ -13,7 +13,7 @@ export const SHARED_NODE_PROPERTIES: Partial<Node> = {
};
export const useBuildNode = () => {
const nodeTemplates = useAppSelector((s) => s.nodeTemplates.templates);
const nodeTemplates = useAppSelector((s) => s.nodes.templates);
const flow = useReactFlow();

View File

@ -1,28 +1,24 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { EMPTY_ARRAY } from 'app/store/util';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeTemplate } from 'features/nodes/store/selectors';
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
import { TEMPLATE_BUILDER_MAP } from 'features/nodes/util/schema/buildFieldInputTemplate';
import { keys, map } from 'lodash-es';
import { useMemo } from 'react';
export const useConnectionInputFieldNames = (nodeId: string) => {
export const useConnectionInputFieldNames = (nodeId: string): string[] => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return [];
}
const nodeTemplate = nodeTemplates.templates[node.data.type];
if (!nodeTemplate) {
return [];
createMemoizedSelector(selectNodesSlice, (nodes) => {
const template = selectNodeTemplate(nodes, nodeId);
if (!template) {
return EMPTY_ARRAY;
}
// get the visible fields
const fields = map(nodeTemplate.inputs).filter(
const fields = map(template.inputs).filter(
(field) =>
(field.input === 'connection' && !field.type.isCollectionOrScalar) ||
!keys(TEMPLATE_BUILDER_MAP).includes(field.type.name)

View File

@ -14,7 +14,7 @@ const selectIsConnectionInProgress = createSelector(
export type UseConnectionStateProps = {
nodeId: string;
fieldName: string;
kind: 'input' | 'output';
kind: 'inputs' | 'outputs';
};
export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionStateProps) => {
@ -26,8 +26,8 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta
Boolean(
nodes.edges.filter((edge) => {
return (
(kind === 'input' ? edge.target : edge.source) === nodeId &&
(kind === 'input' ? edge.targetHandle : edge.sourceHandle) === fieldName
(kind === 'inputs' ? edge.target : edge.source) === nodeId &&
(kind === 'inputs' ? edge.targetHandle : edge.sourceHandle) === fieldName
);
}).length
)
@ -36,7 +36,7 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta
);
const selectConnectionError = useMemo(
() => makeConnectionErrorSelector(nodeId, fieldName, kind === 'input' ? 'target' : 'source', fieldType),
() => makeConnectionErrorSelector(nodeId, fieldName, kind === 'inputs' ? 'target' : 'source', fieldType),
[nodeId, fieldName, kind, fieldType]
);
@ -46,7 +46,7 @@ export const useConnectionState = ({ nodeId, fieldName, kind }: UseConnectionSta
Boolean(
nodes.connectionStartParams?.nodeId === nodeId &&
nodes.connectionStartParams?.handleId === fieldName &&
nodes.connectionStartParams?.handleType === { input: 'target', output: 'source' }[kind]
nodes.connectionStartParams?.handleType === { inputs: 'target', outputs: 'source' }[kind]
)
),
[fieldName, kind, nodeId]

View File

@ -2,23 +2,19 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { compareVersions } from 'compare-versions';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeData, selectNodeTemplate } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useDoNodeVersionsMatch = (nodeId: string) => {
export const useDoNodeVersionsMatch = (nodeId: string): boolean => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
createSelector(selectNodesSlice, (nodes) => {
const data = selectNodeData(nodes, nodeId);
const template = selectNodeTemplate(nodes, nodeId);
if (!template?.version || !data?.version) {
return false;
}
const nodeTemplate = nodeTemplates.templates[node?.data.type ?? ''];
if (!nodeTemplate?.version || !node.data?.version) {
return false;
}
return compareVersions(nodeTemplate.version, node.data.version) === 0;
return compareVersions(template.version, data.version) === 0;
}),
[nodeId]
);

View File

@ -1,18 +1,18 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeData } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useDoesInputHaveValue = (nodeId: string, fieldName: string) => {
export const useDoesInputHaveValue = (nodeId: string, fieldName: string): boolean => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
const data = selectNodeData(nodes, nodeId);
if (!data) {
return false;
}
return node?.data.inputs[fieldName]?.value !== undefined;
return data.inputs[fieldName]?.value !== undefined;
}),
[fieldName, nodeId]
);

View File

@ -1,23 +0,0 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useFieldInstance = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return node?.data.inputs[fieldName];
}),
[fieldName, nodeId]
);
const fieldData = useAppSelector(selector);
return fieldData;
};

View File

@ -1,23 +1,20 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectFieldInputInstance } from 'features/nodes/store/selectors';
import type { FieldInputInstance } from 'features/nodes/types/field';
import { useMemo } from 'react';
export const useFieldInputInstance = (nodeId: string, fieldName: string) => {
export const useFieldInputInstance = (nodeId: string, fieldName: string): FieldInputInstance | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return node.data.inputs[fieldName];
return selectFieldInputInstance(nodes, nodeId, fieldName);
}),
[fieldName, nodeId]
);
const fieldTemplate = useAppSelector(selector);
const fieldData = useAppSelector(selector);
return fieldTemplate;
return fieldData;
};

View File

@ -1,21 +1,16 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectFieldInputTemplate } from 'features/nodes/store/selectors';
import type { FieldInput } from 'features/nodes/types/field';
import { useMemo } from 'react';
export const useFieldInputKind = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
const nodeTemplate = nodeTemplates.templates[node?.data.type ?? ''];
const fieldTemplate = nodeTemplate?.inputs[fieldName];
return fieldTemplate?.input;
createSelector(selectNodesSlice, (nodes): FieldInput | null => {
const template = selectFieldInputTemplate(nodes, nodeId, fieldName);
return template?.input ?? null;
}),
[fieldName, nodeId]
);

View File

@ -1,20 +1,15 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectFieldInputTemplate } from 'features/nodes/store/selectors';
import type { FieldInputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
export const useFieldInputTemplate = (nodeId: string, fieldName: string) => {
export const useFieldInputTemplate = (nodeId: string, fieldName: string): FieldInputTemplate | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
const nodeTemplate = nodeTemplates.templates[node?.data.type ?? ''];
return nodeTemplate?.inputs[fieldName];
createMemoizedSelector(selectNodesSlice, (nodes) => {
return selectFieldInputTemplate(nodes, nodeId, fieldName);
}),
[fieldName, nodeId]
);

View File

@ -1,18 +1,14 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectFieldInputInstance } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useFieldLabel = (nodeId: string, fieldName: string) => {
export const useFieldLabel = (nodeId: string, fieldName: string): string | null => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return node?.data.inputs[fieldName]?.label;
return selectFieldInputInstance(nodes, nodeId, fieldName)?.label ?? null;
}),
[fieldName, nodeId]
);

View File

@ -1,23 +0,0 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useFieldOutputInstance = (nodeId: string, fieldName: string) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
return node.data.outputs[fieldName];
}),
[fieldName, nodeId]
);
const fieldTemplate = useAppSelector(selector);
return fieldTemplate;
};

View File

@ -1,20 +1,15 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectFieldOutputTemplate } from 'features/nodes/store/selectors';
import type { FieldOutputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
export const useFieldOutputTemplate = (nodeId: string, fieldName: string) => {
export const useFieldOutputTemplate = (nodeId: string, fieldName: string): FieldOutputTemplate | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
}
const nodeTemplate = nodeTemplates.templates[node?.data.type ?? ''];
return nodeTemplate?.outputs[fieldName];
createMemoizedSelector(selectNodesSlice, (nodes) => {
return selectFieldOutputTemplate(nodes, nodeId, fieldName);
}),
[fieldName, nodeId]
);

View File

@ -1,21 +1,22 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { KIND_MAP } from 'features/nodes/types/constants';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors';
import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field';
import { useMemo } from 'react';
export const useFieldTemplate = (nodeId: string, fieldName: string, kind: 'input' | 'output') => {
export const useFieldTemplate = (
nodeId: string,
fieldName: string,
kind: 'inputs' | 'outputs'
): FieldInputTemplate | FieldOutputTemplate | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
createMemoizedSelector(selectNodesSlice, (nodes) => {
if (kind === 'inputs') {
return selectFieldInputTemplate(nodes, nodeId, fieldName);
}
const nodeTemplate = nodeTemplates.templates[node?.data.type ?? ''];
return nodeTemplate?.[KIND_MAP[kind]][fieldName];
return selectFieldOutputTemplate(nodes, nodeId, fieldName);
}),
[fieldName, kind, nodeId]
);

View File

@ -1,21 +1,17 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { KIND_MAP } from 'features/nodes/types/constants';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'input' | 'output') => {
export const useFieldTemplateTitle = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): string | null => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
createSelector(selectNodesSlice, (nodes) => {
if (kind === 'inputs') {
return selectFieldInputTemplate(nodes, nodeId, fieldName)?.title ?? null;
}
const nodeTemplate = nodeTemplates.templates[node?.data.type ?? ''];
return nodeTemplate?.[KIND_MAP[kind]][fieldName]?.title;
return selectFieldOutputTemplate(nodes, nodeId, fieldName)?.title ?? null;
}),
[fieldName, kind, nodeId]
);

View File

@ -1,20 +1,18 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { KIND_MAP } from 'features/nodes/types/constants';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectFieldInputTemplate, selectFieldOutputTemplate } from 'features/nodes/store/selectors';
import type { FieldType } from 'features/nodes/types/field';
import { useMemo } from 'react';
export const useFieldType = (nodeId: string, fieldName: string, kind: 'input' | 'output') => {
export const useFieldType = (nodeId: string, fieldName: string, kind: 'inputs' | 'outputs'): FieldType | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return;
if (kind === 'inputs') {
return selectFieldInputTemplate(nodes, nodeId, fieldName)?.type ?? null;
}
const field = node.data[KIND_MAP[kind]][fieldName];
return field?.type;
return selectFieldOutputTemplate(nodes, nodeId, fieldName)?.type ?? null;
}),
[fieldName, kind, nodeId]
);

View File

@ -1,13 +1,12 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
const selector = createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) =>
const selector = createSelector(selectNodesSlice, (nodes) =>
nodes.nodes.filter(isInvocationNode).some((node) => {
const template = nodeTemplates.templates[node.data.type];
const template = nodes.templates[node.data.type];
if (!template) {
return false;
}

View File

@ -1,24 +1,21 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeTemplate } from 'features/nodes/store/selectors';
import { some } from 'lodash-es';
import { useMemo } from 'react';
export const useHasImageOutput = (nodeId: string) => {
export const useHasImageOutput = (nodeId: string): boolean => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
const template = selectNodeTemplate(nodes, nodeId);
return some(
node.data.outputs,
template?.outputs,
(output) =>
output.type.name === 'ImageField' &&
// the image primitive node (node type "image") does not actually save the image, do not show the image-saving checkboxes
node.data.type !== 'image'
template?.type !== 'image'
);
}),
[nodeId]

View File

@ -1,18 +1,14 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeData } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useIsIntermediate = (nodeId: string) => {
export const useIsIntermediate = (nodeId: string): boolean => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
return node.data.isIntermediate;
return selectNodeData(nodes, nodeId)?.isIntermediate ?? false;
}),
[nodeId]
);

View File

@ -1,11 +1,10 @@
// TODO: enable this at some point
import { useAppSelector } from 'app/store/storeHooks';
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { getIsGraphAcyclic } from 'features/nodes/store/util/getIsGraphAcyclic';
import { validateSourceAndTargetTypes } from 'features/nodes/store/util/validateSourceAndTargetTypes';
import type { InvocationNodeData } from 'features/nodes/types/invocation';
import { useCallback } from 'react';
import type { Connection, Node } from 'reactflow';
import { useReactFlow } from 'reactflow';
/**
* NOTE: The logic here must be duplicated in `invokeai/frontend/web/src/features/nodes/store/util/makeIsConnectionValidSelector.ts`
@ -13,39 +12,34 @@ import { useReactFlow } from 'reactflow';
*/
export const useIsValidConnection = () => {
const flow = useReactFlow();
const store = useAppStore();
const shouldValidateGraph = useAppSelector((s) => s.nodes.shouldValidateGraph);
const isValidConnection = useCallback(
({ source, sourceHandle, target, targetHandle }: Connection): boolean => {
const edges = flow.getEdges();
const nodes = flow.getNodes();
// Connection must have valid targets
if (!(source && sourceHandle && target && targetHandle)) {
return false;
}
// Find the source and target nodes
const sourceNode = flow.getNode(source) as Node<InvocationNodeData>;
const targetNode = flow.getNode(target) as Node<InvocationNodeData>;
// Conditional guards against undefined nodes/handles
if (!(sourceNode && targetNode && sourceNode.data && targetNode.data)) {
return false;
}
const sourceField = sourceNode.data.outputs[sourceHandle];
const targetField = targetNode.data.inputs[targetHandle];
if (!sourceField || !targetField) {
// something has gone terribly awry
return false;
}
if (source === target) {
// Don't allow nodes to connect to themselves, even if validation is disabled
return false;
}
const state = store.getState();
const { nodes, edges, templates } = state.nodes;
// Find the source and target nodes
const sourceNode = nodes.find((node) => node.id === source) as Node<InvocationNodeData>;
const targetNode = nodes.find((node) => node.id === target) as Node<InvocationNodeData>;
const sourceFieldTemplate = templates[sourceNode.data.type]?.outputs[sourceHandle];
const targetFieldTemplate = templates[targetNode.data.type]?.inputs[targetHandle];
// Conditional guards against undefined nodes/handles
if (!(sourceFieldTemplate && targetFieldTemplate)) {
return false;
}
if (!shouldValidateGraph) {
// manual override!
return true;
@ -69,20 +63,20 @@ export const useIsValidConnection = () => {
return edge.target === target && edge.targetHandle === targetHandle;
}) &&
// except CollectionItem inputs can have multiples
targetField.type.name !== 'CollectionItemField'
targetFieldTemplate.type.name !== 'CollectionItemField'
) {
return false;
}
// Must use the originalType here if it exists
if (!validateSourceAndTargetTypes(sourceField.type, targetField.type)) {
if (!validateSourceAndTargetTypes(sourceFieldTemplate.type, targetFieldTemplate.type)) {
return false;
}
// Graphs much be acyclic (no loops!)
return getIsGraphAcyclic(source, target, nodes, edges);
},
[flow, shouldValidateGraph]
[shouldValidateGraph, store]
);
return isValidConnection;

View File

@ -1,20 +1,15 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeTemplate } from 'features/nodes/store/selectors';
import type { Classification } from 'features/nodes/types/common';
import { useMemo } from 'react';
export const useNodeClassification = (nodeId: string) => {
export const useNodeClassification = (nodeId: string): Classification | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
const nodeTemplate = nodeTemplates.templates[node?.data.type ?? ''];
return nodeTemplate?.classification;
createSelector(selectNodesSlice, (nodes) => {
return selectNodeTemplate(nodes, nodeId)?.classification ?? null;
}),
[nodeId]
);

View File

@ -1,14 +1,15 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeData } from 'features/nodes/store/selectors';
import type { InvocationNodeData } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useNodeData = (nodeId: string) => {
export const useNodeData = (nodeId: string): InvocationNodeData | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
return node?.data;
return selectNodeData(nodes, nodeId);
}),
[nodeId]
);

View File

@ -1,19 +1,14 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeData } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useNodeLabel = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
return node.data.label;
return selectNodeData(nodes, nodeId)?.label ?? null;
}),
[nodeId]
);

View File

@ -1,21 +1,20 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectInvocationNode, selectNodeTemplate } from 'features/nodes/store/selectors';
import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate';
import { useMemo } from 'react';
export const useNodeNeedsUpdate = (nodeId: string) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
const template = nodeTemplates.templates[node?.data.type ?? ''];
if (isInvocationNode(node) && template) {
return getNeedsUpdate(node, template);
createMemoizedSelector(selectNodesSlice, (nodes) => {
const node = selectInvocationNode(nodes, nodeId);
const template = selectNodeTemplate(nodes, nodeId);
if (!node || !template) {
return false;
}
return false;
return getNeedsUpdate(node, template);
}),
[nodeId]
);

View File

@ -1,18 +1,14 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeData } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useNodePack = (nodeId: string) => {
export const useNodePack = (nodeId: string): string | null => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
return node.data.nodePack;
return selectNodeData(nodes, nodeId)?.nodePack ?? null;
}),
[nodeId]
);

View File

@ -1,16 +1,15 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { selectNodeTemplate } from 'features/nodes/store/selectors';
import type { InvocationTemplate } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useNodeTemplate = (nodeId: string) => {
export const useNodeTemplate = (nodeId: string): InvocationTemplate | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
const nodeTemplate = nodeTemplates.templates[node?.data.type ?? ''];
return nodeTemplate;
createSelector(selectNodesSlice, (nodes) => {
return selectNodeTemplate(nodes, nodeId);
}),
[nodeId]
);

View File

@ -1,14 +1,14 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import type { InvocationTemplate } from 'features/nodes/types/invocation';
import { useMemo } from 'react';
export const useNodeTemplateByType = (type: string) => {
export const useNodeTemplateByType = (type: string): InvocationTemplate | null => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodeTemplatesSlice, (nodeTemplates): InvocationTemplate | undefined => {
return nodeTemplates.templates[type];
createSelector(selectNodesSlice, (nodes) => {
return nodes.templates[type] ?? null;
}),
[type]
);

View File

@ -1,21 +1,14 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeTemplate } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useNodeTemplateTitle = (nodeId: string) => {
export const useNodeTemplateTitle = (nodeId: string): string | null => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
const nodeTemplate = node ? nodeTemplates.templates[node.data.type] : undefined;
return nodeTemplate?.title;
createSelector(selectNodesSlice, (nodes) => {
return selectNodeTemplate(nodes, nodeId)?.title ?? null;
}),
[nodeId]
);

View File

@ -1,8 +1,8 @@
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { EMPTY_ARRAY } from 'app/store/util';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectNodeTemplatesSlice } from 'features/nodes/store/nodeTemplatesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeTemplate } from 'features/nodes/store/selectors';
import { getSortedFilteredFieldNames } from 'features/nodes/util/node/getSortedFilteredFieldNames';
import { map } from 'lodash-es';
import { useMemo } from 'react';
@ -10,17 +10,13 @@ import { useMemo } from 'react';
export const useOutputFieldNames = (nodeId: string) => {
const selector = useMemo(
() =>
createMemoizedSelector(selectNodesSlice, selectNodeTemplatesSlice, (nodes, nodeTemplates) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return [];
}
const nodeTemplate = nodeTemplates.templates[node.data.type];
if (!nodeTemplate) {
return [];
createSelector(selectNodesSlice, (nodes) => {
const template = selectNodeTemplate(nodes, nodeId);
if (!template) {
return EMPTY_ARRAY;
}
return getSortedFilteredFieldNames(map(nodeTemplate.outputs));
return getSortedFilteredFieldNames(map(template.outputs));
}),
[nodeId]
);

View File

@ -1,18 +1,14 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { isInvocationNode } from 'features/nodes/types/invocation';
import { selectNodeData } from 'features/nodes/store/selectors';
import { useMemo } from 'react';
export const useUseCache = (nodeId: string) => {
const selector = useMemo(
() =>
createSelector(selectNodesSlice, (nodes) => {
const node = nodes.nodes.find((node) => node.id === nodeId);
if (!isInvocationNode(node)) {
return false;
}
return node.data.useCache;
return selectNodeData(nodes, nodeId)?.useCache ?? false;
}),
[nodeId]
);

View File

@ -2,14 +2,14 @@ import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
import type { WorkflowV2 } from 'features/nodes/types/workflow';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
import type { BuildWorkflowArg } from 'features/nodes/util/workflow/buildWorkflow';
import { buildWorkflowFast } from 'features/nodes/util/workflow/buildWorkflow';
import { debounce } from 'lodash-es';
import { atom } from 'nanostores';
import { useEffect } from 'react';
export const $builtWorkflow = atom<WorkflowV2 | null>(null);
export const $builtWorkflow = atom<WorkflowV3 | null>(null);
const debouncedBuildWorkflow = debounce((arg: BuildWorkflowArg) => {
$builtWorkflow.set(buildWorkflowFast(arg));