mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): initial values for form fields (WIP)
This commit is contained in:
@@ -3,7 +3,7 @@ import { Box, Circle, Flex, IconButton, Spacer } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { DndListDropIndicator } from 'features/dnd/DndListDropIndicator';
|
||||
import { InputFieldDescriptionPopover } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldDescriptionPopover';
|
||||
import { InputFieldResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton';
|
||||
import { InputFieldResetToInitialLinearViewValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialLinearViewValueIconButton';
|
||||
import { useLinearViewFieldDnd } from 'features/nodes/components/sidePanel/workflow/useLinearViewFieldDnd';
|
||||
import { useMouseOverNode } from 'features/nodes/hooks/useMouseOverNode';
|
||||
import { workflowExposedFieldRemoved } from 'features/nodes/store/workflowSlice';
|
||||
@@ -61,7 +61,7 @@ export const InputFieldEditModeLinear = memo(({ nodeId, fieldName }: Props) => {
|
||||
<Spacer />
|
||||
{isMouseOverNode && <Circle me={2} size={2} borderRadius="full" bg="invokeBlue.500" />}
|
||||
<InputFieldDescriptionPopover nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldResetToInitialValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldResetToInitialLinearViewValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<IconButton
|
||||
aria-label={t('nodes.removeLinearView')}
|
||||
tooltip={t('nodes.removeLinearView')}
|
||||
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
fieldName: string;
|
||||
};
|
||||
|
||||
export const InputFieldResetToInitialValueIconButton = memo(({ nodeId, fieldName }: Props) => {
|
||||
export const InputFieldResetToInitialLinearViewValueIconButton = memo(({ nodeId, fieldName }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isValueChanged, resetToInitialLinearViewValue } = useInputFieldInitialLinearViewValue(nodeId, fieldName);
|
||||
|
||||
@@ -27,4 +27,4 @@ export const InputFieldResetToInitialValueIconButton = memo(({ nodeId, fieldName
|
||||
);
|
||||
});
|
||||
|
||||
InputFieldResetToInitialValueIconButton.displayName = 'InputFieldResetToInitialValueIconButton';
|
||||
InputFieldResetToInitialLinearViewValueIconButton.displayName = 'InputFieldResetToInitialLinearViewValueIconButton';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, FormControl, FormLabel, Spacer } from '@invoke-ai/ui-library';
|
||||
import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer';
|
||||
import { InputFieldResetToInitialValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialValueIconButton';
|
||||
import { InputFieldResetToInitialLinearViewValueIconButton } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldResetToInitialLinearViewValueIconButton';
|
||||
import { useInputFieldLabel } from 'features/nodes/hooks/useInputFieldLabel';
|
||||
import { useInputFieldTemplateTitle } from 'features/nodes/hooks/useInputFieldTemplateTitle';
|
||||
import { memo } from 'react';
|
||||
@@ -19,7 +19,7 @@ export const InputFieldViewMode = memo(({ nodeId, fieldName }: Props) => {
|
||||
<FormLabel fontSize="sm" display="flex" w="full" m={0} gap={2} ps={1}>
|
||||
{label || fieldTemplateTitle}
|
||||
<Spacer />
|
||||
<InputFieldResetToInitialValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
<InputFieldResetToInitialLinearViewValueIconButton nodeId={nodeId} fieldName={fieldName} />
|
||||
</FormLabel>
|
||||
<Box w="full" h="full">
|
||||
<InputFieldRenderer nodeId={nodeId} fieldName={fieldName} />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useInputFieldInitialFormValue } from 'features/nodes/hooks/useInputFieldInitialFormValue';
|
||||
import type { NodeFieldElement } from 'features/nodes/types/workflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
|
||||
type Props = {
|
||||
element: NodeFieldElement;
|
||||
};
|
||||
|
||||
export const NodeFieldElementResetToInitialValueIconButton = memo(({ element }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { id, data } = element;
|
||||
const { nodeId, fieldName } = data.fieldIdentifier;
|
||||
const { isValueChanged, resetToInitialValue } = useInputFieldInitialFormValue(id, nodeId, fieldName);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant="link"
|
||||
size="sm"
|
||||
alignSelf="stretch"
|
||||
tooltip={t('nodes.resetToDefaultValue')}
|
||||
aria-label={t('nodes.resetToDefaultValue')}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
onClick={resetToInitialValue}
|
||||
isDisabled={!isValueChanged}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
NodeFieldElementResetToInitialValueIconButton.displayName = 'NodeFieldElementResetToInitialValueIconButton';
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Flex, forwardRef, IconButton, Spacer, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import {
|
||||
NodeFieldElementResetToInitialValueIconButton,
|
||||
} from 'features/nodes/components/flow/nodes/Invocation/fields/NodeFieldElementResetToInitialValueIconButton';
|
||||
import { ContainerElementSettings } from 'features/nodes/components/sidePanel/builder/ContainerElementSettings';
|
||||
import { useDepthContext } from 'features/nodes/components/sidePanel/builder/contexts';
|
||||
import { NodeFieldElementSettings } from 'features/nodes/components/sidePanel/builder/NodeFieldElementSettings';
|
||||
@@ -52,6 +55,7 @@ export const FormElementEditModeHeader = memo(
|
||||
<Spacer />
|
||||
{isContainerElement(element) && <ContainerElementSettings element={element} />}
|
||||
{isNodeFieldElement(element) && <NodeFieldElementSettings element={element} />}
|
||||
{isNodeFieldElement(element) && <NodeFieldElementResetToInitialValueIconButton element={element} />}
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
onClick={removeElement}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useInputFieldValue } from 'features/nodes/hooks/useInputFieldValue';
|
||||
import { fieldValueReset } from 'features/nodes/store/nodesSlice';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
const uniqueNonexistentValue = Symbol('uniqueNonexistentValue');
|
||||
|
||||
export const useInputFieldInitialFormValue = (elementId: string, nodeId: string, fieldName: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectInitialValue = useMemo(
|
||||
() =>
|
||||
createSelector(selectWorkflowSlice, (workflow) => {
|
||||
if (!(elementId in workflow.formFieldInitialValues)) {
|
||||
return uniqueNonexistentValue;
|
||||
}
|
||||
return workflow.formFieldInitialValues[elementId];
|
||||
}),
|
||||
[elementId]
|
||||
);
|
||||
const initialValue = useAppSelector(selectInitialValue);
|
||||
const value = useInputFieldValue(nodeId, fieldName);
|
||||
const isValueChanged = useMemo(
|
||||
() => initialValue !== uniqueNonexistentValue && !isEqual(value, initialValue),
|
||||
[value, initialValue]
|
||||
);
|
||||
const resetToInitialValue = useCallback(() => {
|
||||
if (initialValue === uniqueNonexistentValue) {
|
||||
return;
|
||||
}
|
||||
dispatch(fieldValueReset({ nodeId, fieldName, value: initialValue }));
|
||||
}, [dispatch, fieldName, nodeId, initialValue]);
|
||||
|
||||
return { initialValue, isValueChanged, resetToInitialValue };
|
||||
};
|
||||
@@ -40,4 +40,5 @@ export type WorkflowsState = Omit<WorkflowV3, 'nodes' | 'edges'> & {
|
||||
orderBy?: WorkflowRecordOrderBy;
|
||||
orderDirection: SQLiteDirection;
|
||||
categorySections: Record<string, boolean>;
|
||||
formFieldInitialValues: Record<string, StatefulFieldValue>;
|
||||
};
|
||||
|
||||
@@ -10,10 +10,11 @@ import type {
|
||||
WorkflowMode,
|
||||
WorkflowsState as WorkflowState,
|
||||
} from 'features/nodes/store/types';
|
||||
import type { FieldIdentifier } from 'features/nodes/types/field';
|
||||
import type { FieldIdentifier, StatefulFieldValue } from 'features/nodes/types/field';
|
||||
import { isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import type {
|
||||
ContainerElement,
|
||||
ElementId,
|
||||
FormElement,
|
||||
HeadingElement,
|
||||
NodeFieldElement,
|
||||
@@ -70,6 +71,7 @@ const initialWorkflowState: WorkflowState = {
|
||||
isTouched: false,
|
||||
mode: 'view',
|
||||
originalExposedFieldValues: [],
|
||||
formFieldInitialValues: {},
|
||||
searchTerm: '',
|
||||
orderBy: undefined, // initial value is decided in component
|
||||
orderDirection: 'DESC',
|
||||
@@ -165,6 +167,17 @@ export const workflowSlice = createSlice({
|
||||
layout: [],
|
||||
};
|
||||
},
|
||||
formElementNodeFieldInitialValueChanged: (
|
||||
state,
|
||||
action: PayloadAction<{ id: ElementId; value: StatefulFieldValue }>
|
||||
) => {
|
||||
const { id, value } = action.payload;
|
||||
const element = state.form?.elements[id];
|
||||
if (!element || !isNodeFieldElement(element)) {
|
||||
return;
|
||||
}
|
||||
state.formFieldInitialValues[id] = value;
|
||||
},
|
||||
formElementAdded: (
|
||||
state,
|
||||
action: PayloadAction<{ element: FormElement; containerId?: string; index?: number }>
|
||||
@@ -183,6 +196,7 @@ export const workflowSlice = createSlice({
|
||||
}
|
||||
const { id } = action.payload;
|
||||
removeElement({ id, formState: state.form });
|
||||
delete state.formFieldInitialValues[id];
|
||||
},
|
||||
formRootReordered: (state, action: PayloadAction<{ layout: string[] }>) => {
|
||||
const { layout } = action.payload;
|
||||
@@ -256,10 +270,34 @@ export const workflowSlice = createSlice({
|
||||
originalExposedFieldValues.push(originalExposedFieldValue);
|
||||
});
|
||||
|
||||
const formFieldInitialValues: Record<string, StatefulFieldValue> = {};
|
||||
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = node.data.inputs[fieldName];
|
||||
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
formFieldInitialValues[el.id] = field.value;
|
||||
}
|
||||
|
||||
return {
|
||||
...deepClone(initialWorkflowState),
|
||||
...deepClone(workflowExtra),
|
||||
originalExposedFieldValues,
|
||||
formFieldInitialValues,
|
||||
mode: state.mode,
|
||||
};
|
||||
});
|
||||
@@ -360,6 +398,7 @@ export const {
|
||||
formElementHeadingDataChanged,
|
||||
formElementTextDataChanged,
|
||||
formElementNodeFieldDataChanged,
|
||||
formElementNodeFieldInitialValueChanged,
|
||||
formElementContainerDataChanged,
|
||||
formModeChanged,
|
||||
} = workflowSlice.actions;
|
||||
@@ -501,7 +540,7 @@ const addElement = (args: {
|
||||
element: FormElement;
|
||||
containerId?: string;
|
||||
index?: number;
|
||||
}) => {
|
||||
}): boolean => {
|
||||
const { formState, element, containerId, index } = args;
|
||||
const { elements } = formState;
|
||||
|
||||
@@ -510,15 +549,16 @@ const addElement = (args: {
|
||||
element.parentId = undefined;
|
||||
formState.elements[element.id] = element;
|
||||
formState.layout.splice(index ?? formState.layout.length, 0, element.id);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
const container = elements[containerId];
|
||||
if (!container || !isContainerElement(container)) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
element.parentId = containerId;
|
||||
elements[element.id] = element;
|
||||
container.data.children.splice(index ?? container.data.children.length, 0, element.id);
|
||||
return true;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user