feat(ui): initial values for form fields (WIP)

This commit is contained in:
psychedelicious
2025-02-07 16:14:16 +11:00
parent aed802fa74
commit 1104d2a00f
8 changed files with 124 additions and 10 deletions

View File

@@ -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')}

View File

@@ -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';

View File

@@ -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} />

View File

@@ -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';

View File

@@ -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}

View File

@@ -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 };
};

View File

@@ -40,4 +40,5 @@ export type WorkflowsState = Omit<WorkflowV3, 'nodes' | 'edges'> & {
orderBy?: WorkflowRecordOrderBy;
orderDirection: SQLiteDirection;
categorySections: Record<string, boolean>;
formFieldInitialValues: Record<string, StatefulFieldValue>;
};

View File

@@ -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;
};