diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 59afbec81e..40cd51e0fc 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -10,10 +10,11 @@ import { } from 'features/nodes/components/sidePanel/builder/form-manipulation'; import { workflowLoaded } from 'features/nodes/store/actions'; import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged } from 'features/nodes/store/nodesSlice'; -import type { WorkflowMode, WorkflowsState as WorkflowState } from 'features/nodes/store/types'; +import type { NodesState, WorkflowMode, WorkflowsState as WorkflowState } from 'features/nodes/store/types'; import type { FieldIdentifier, StatefulFieldValue } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; import type { + BuilderForm, ContainerElement, ElementId, FormElement, @@ -188,35 +189,19 @@ export const workflowSlice = createSlice({ formElementContainerDataChanged: (state, action: FormElementDataChangedAction) => { formElementDataChangedReducer(state, action, isContainerElement); }, + formFieldInitialValuesChanged: ( + state, + action: PayloadAction<{ formFieldInitialValues: WorkflowState['formFieldInitialValues'] }> + ) => { + const { formFieldInitialValues } = action.payload; + state.formFieldInitialValues = formFieldInitialValues; + }, }, extraReducers: (builder) => { builder.addCase(workflowLoaded, (state, action): WorkflowState => { const { nodes, edges: _edges, ...workflowExtra } = action.payload; - const formFieldInitialValues: Record = {}; - - if (workflowExtra.form) { - for (const el of Object.values(workflowExtra.form.elements)) { - if (!isNodeFieldElement(el)) { - continue; - } - const { nodeId, fieldName } = el.data.fieldIdentifier; - - const node = nodes.find((n) => n.id === nodeId); - - if (!isInvocationNode(node)) { - continue; - } - - const field = node.data.inputs[fieldName]; - - if (!field) { - continue; - } - - formFieldInitialValues[el.id] = field.value; - } - } + const formFieldInitialValues = getFormFieldInitialValues(workflowExtra.form, nodes); return { ...deepClone(initialWorkflowState), @@ -322,6 +307,7 @@ export const { formElementTextDataChanged, formElementNodeFieldDataChanged, formElementContainerDataChanged, + formFieldInitialValuesChanged, } = workflowSlice.actions; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -343,6 +329,34 @@ export const selectWorkflowSlice = (state: RootState) => state.workflow; const createWorkflowSelector = (selector: Selector) => createSelector(selectWorkflowSlice, selector); +// The form builder's initial values are based on the current values of the node fields in the workflow. +export const getFormFieldInitialValues = (form: BuilderForm, nodes: NodesState['nodes']) => { + const formFieldInitialValues: Record = {}; + + for (const el of Object.values(form.elements)) { + if (!isNodeFieldElement(el)) { + continue; + } + const { nodeId, fieldName } = el.data.fieldIdentifier; + + const node = nodes.find((n) => n.id === nodeId); + + if (!isInvocationNode(node)) { + continue; + } + + const field = node.data.inputs[fieldName]; + + if (!field) { + continue; + } + + formFieldInitialValues[el.id] = field.value; + } + + return formFieldInitialValues; +}; + export const selectWorkflowName = createWorkflowSelector((workflow) => workflow.name); export const selectWorkflowId = createWorkflowSelector((workflow) => workflow.id); export const selectWorkflowMode = createWorkflowSelector((workflow) => workflow.mode); @@ -351,6 +365,7 @@ export const selectWorkflowSearchTerm = createWorkflowSelector((workflow) => wor export const selectWorkflowOrderBy = createWorkflowSelector((workflow) => workflow.orderBy); export const selectWorkflowOrderDirection = createWorkflowSelector((workflow) => workflow.orderDirection); export const selectWorkflowDescription = createWorkflowSelector((workflow) => workflow.description); +export const selectWorkflowForm = createWorkflowSelector((workflow) => workflow.form); export const selectCleanEditor = createSelector([selectNodesSlice, selectWorkflowSlice], (nodes, workflow) => { const noNodes = !nodes.nodes.length; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetFormInitialValues.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetFormInitialValues.ts new file mode 100644 index 0000000000..d7546c8c31 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetFormInitialValues.ts @@ -0,0 +1,19 @@ +import { useAppStore } from 'app/store/nanostores/store'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; +import { + getFormFieldInitialValues as _getFormFieldInitialValues, + selectWorkflowForm, +} from 'features/nodes/store/workflowSlice'; +import { useCallback } from 'react'; + +export const useGetFormFieldInitialValues = () => { + const store = useAppStore(); + + const getFormFieldInitialValues = useCallback(() => { + const form = selectWorkflowForm(store.getState()); + const { nodes } = selectNodesSlice(store.getState()); + return _getFormFieldInitialValues(form, nodes); + }, [store]); + + return getFormFieldInitialValues; +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts index 5854a9dd30..53119c051e 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts @@ -2,8 +2,9 @@ import type { ToastId } from '@invoke-ai/ui-library'; import { useToast } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; -import { workflowIDChanged, workflowSaved } from 'features/nodes/store/workflowSlice'; +import { formFieldInitialValuesChanged, workflowIDChanged, workflowSaved } from 'features/nodes/store/workflowSlice'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; +import { useGetFormFieldInitialValues } from 'features/workflowLibrary/hooks/useGetFormInitialValues'; import { workflowUpdated } from 'features/workflowLibrary/store/actions'; import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -24,6 +25,7 @@ export const isWorkflowWithID = (workflow: WorkflowV3): workflow is SetRequired< export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); + const getFormFieldInitialValues = useGetFormFieldInitialValues(); const [updateWorkflow, updateWorkflowResult] = useUpdateWorkflowMutation(); const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); const toast = useToast(); @@ -48,6 +50,8 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { dispatch(workflowIDChanged(data.workflow.id)); } dispatch(workflowSaved()); + // When a workflow is saved, the form field initial values are updated to the current form field values + dispatch(formFieldInitialValuesChanged({ formFieldInitialValues: getFormFieldInitialValues() })); toast.update(toastRef.current, { title: t('workflows.workflowSaved'), status: 'success', @@ -69,7 +73,7 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { toast.close(toastRef.current); } } - }, [updateWorkflow, dispatch, toast, t, createWorkflow]); + }, [toast, t, dispatch, getFormFieldInitialValues, updateWorkflow, createWorkflow]); return { saveWorkflow, isLoading: updateWorkflowResult.isLoading || createWorkflowResult.isLoading, diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts index 33fd5545e6..2c18fcdd90 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts @@ -3,12 +3,14 @@ import { useToast } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; import { + formFieldInitialValuesChanged, workflowCategoryChanged, workflowIDChanged, workflowNameChanged, workflowSaved, } from 'features/nodes/store/workflowSlice'; import type { WorkflowCategory } from 'features/nodes/types/workflow'; +import { useGetFormFieldInitialValues } from 'features/workflowLibrary/hooks/useGetFormInitialValues'; import { newWorkflowSaved } from 'features/workflowLibrary/store/actions'; import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; @@ -33,6 +35,8 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation(); + const getFormFieldInitialValues = useGetFormFieldInitialValues(); + const toast = useToast(); const toastRef = useRef(); const saveWorkflowAs = useCallback( @@ -57,6 +61,8 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { dispatch(workflowNameChanged(data.workflow.name)); dispatch(workflowCategoryChanged(data.workflow.meta.category)); dispatch(workflowSaved()); + // When a workflow is saved, the form field initial values are updated to the current form field values + dispatch(formFieldInitialValuesChanged({ formFieldInitialValues: getFormFieldInitialValues() })); dispatch(newWorkflowSaved({ category })); onSuccess && onSuccess(); @@ -80,7 +86,7 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { } } }, - [toast, createWorkflow, dispatch, t] + [toast, t, createWorkflow, dispatch, getFormFieldInitialValues] ); return { saveWorkflowAs,