mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): use auto-add board as default for nodes
Board fields in the workflow editor now default to using the auto-add board by default.
**This is a change in behaviour - previously, we defaulted to no board (i.e. Uncategorized).**
There is some translation needed between the UI field values for a board and what the graph expects.
A "BoardField" is an object in the shape of `{board_id: string}`.
Valid board field values in the graph:
- undefined
- a BoardField
Value UI values and their mapping to the graph values:
- 'none' -> undefined
- 'auto' -> BoardField for the auto-add board, or if the auto-add board is Uncategorized, undefined
- undefined -> undefined (this is a fallback case with the new logic)
- a BoardField -> the same BoardField
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { enqueueRequested } from 'app/store/actions';
|
||||
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import { isBatchNode, isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph';
|
||||
@@ -17,7 +18,8 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) =
|
||||
const state = getState();
|
||||
const nodes = selectNodesSlice(state);
|
||||
const workflow = state.workflow;
|
||||
const graph = buildNodesGraph(nodes);
|
||||
const templates = $templates.get();
|
||||
const graph = buildNodesGraph(state, templates);
|
||||
const builtWorkflow = buildWorkflowWithValidation({
|
||||
nodes: nodes.nodes,
|
||||
edges: nodes.edges,
|
||||
|
||||
@@ -10,50 +10,99 @@ import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
/**
|
||||
* The board field values in the UI do not map 1-to-1 to the values the graph expects.
|
||||
*
|
||||
* The graph value is either an object in the shape of `{board_id: string}` or undefined.
|
||||
*
|
||||
* But in the UI, we have the following options:
|
||||
* - auto: Use the "auto add" board. During graph building, we pull the auto add board ID from the state and use it.
|
||||
* - none: Do not assign a board. In the graph, this is represented as undefined.
|
||||
* - board_id: Assign the specified board. In the graph, this is represented as `{board_id: string}`.
|
||||
*
|
||||
* It's also possible that the UI value is undefined, which may be the case for some older workflows. In this case, we
|
||||
* map it to the "auto" option.
|
||||
*
|
||||
* So there is some translation that needs to happen in both directions - when the user selects a board in the UI, and
|
||||
* when we build the graph. The former is handled in this component, the latter in the `buildNodesGraph` function.
|
||||
*/
|
||||
|
||||
const listAllBoardsQueryArg = { include_archived: true };
|
||||
|
||||
const getBoardValue = (val: string) => {
|
||||
if (val === 'auto' || val === 'none') {
|
||||
return val;
|
||||
}
|
||||
|
||||
return {
|
||||
board_id: val,
|
||||
};
|
||||
};
|
||||
|
||||
const BoardFieldInputComponent = (props: FieldComponentProps<BoardFieldInputInstance, BoardFieldInputTemplate>) => {
|
||||
const { nodeId, field } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { options, hasBoards } = useListAllBoardsQuery(
|
||||
{ include_archived: true },
|
||||
{
|
||||
selectFromResult: ({ data }) => {
|
||||
const options: ComboboxOption[] = [
|
||||
{
|
||||
label: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
].concat(
|
||||
(data ?? []).map(({ board_id, board_name }) => ({
|
||||
label: board_name,
|
||||
value: board_id,
|
||||
}))
|
||||
);
|
||||
return {
|
||||
options,
|
||||
hasBoards: options.length > 1,
|
||||
};
|
||||
},
|
||||
const listAllBoardsQuery = useListAllBoardsQuery(listAllBoardsQueryArg);
|
||||
|
||||
const autoOption = useMemo<ComboboxOption>(() => {
|
||||
return {
|
||||
label: t('common.auto'),
|
||||
value: 'auto',
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
const noneOption = useMemo<ComboboxOption>(() => {
|
||||
return {
|
||||
label: `${t('common.none')} (${t('boards.uncategorized')})`,
|
||||
value: 'none',
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
const options = useMemo<ComboboxOption[]>(() => {
|
||||
const _options: ComboboxOption[] = [autoOption, noneOption];
|
||||
if (listAllBoardsQuery.data) {
|
||||
for (const board of listAllBoardsQuery.data) {
|
||||
_options.push({
|
||||
label: board.board_name,
|
||||
value: board.board_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
return _options;
|
||||
}, [autoOption, listAllBoardsQuery.data, noneOption]);
|
||||
|
||||
const onChange = useCallback<ComboboxOnChange>(
|
||||
(v) => {
|
||||
if (!v) {
|
||||
// This should never happen
|
||||
return;
|
||||
}
|
||||
|
||||
const value = getBoardValue(v.value);
|
||||
|
||||
dispatch(
|
||||
fieldBoardValueChanged({
|
||||
nodeId,
|
||||
fieldName: field.name,
|
||||
value: v.value !== 'none' ? { board_id: v.value } : undefined,
|
||||
value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, field.name, nodeId]
|
||||
);
|
||||
|
||||
const value = useMemo(() => options.find((o) => o.value === field.value?.board_id), [options, field.value]);
|
||||
const value = useMemo(() => {
|
||||
const _value = field.value;
|
||||
if (!_value || _value === 'auto') {
|
||||
return autoOption;
|
||||
}
|
||||
if (_value === 'none') {
|
||||
return noneOption;
|
||||
}
|
||||
const boardOption = options.find((o) => o.value === _value.board_id);
|
||||
return boardOption ?? autoOption;
|
||||
}, [field.value, options, autoOption, noneOption]);
|
||||
|
||||
const noOptionsMessage = useCallback(() => t('boards.noMatching'), [t]);
|
||||
|
||||
@@ -65,7 +114,6 @@ const BoardFieldInputComponent = (props: FieldComponentProps<BoardFieldInputInst
|
||||
onChange={onChange}
|
||||
placeholder={t('boards.selectBoard')}
|
||||
noOptionsMessage={noOptionsMessage}
|
||||
isDisabled={!hasBoards}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -642,7 +642,7 @@ export const isImageFieldCollectionInputTemplate = buildTemplateTypeGuard<ImageF
|
||||
// #endregion
|
||||
|
||||
// #region BoardField
|
||||
export const zBoardFieldValue = zBoardField.optional();
|
||||
export const zBoardFieldValue = z.union([zBoardField, z.enum(['none', 'auto'])]).optional();
|
||||
const zBoardFieldInputInstance = zFieldInputInstanceBase.extend({
|
||||
value: zBoardFieldValue,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { logger } from 'app/logging/logger';
|
||||
import type { NodesState } from 'features/nodes/store/types';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectNodesSlice } from 'features/nodes/store/selectors';
|
||||
import type { Templates } from 'features/nodes/store/types';
|
||||
import type { BoardField } from 'features/nodes/types/common';
|
||||
import type { BoardFieldInputInstance } from 'features/nodes/types/field';
|
||||
import { isBoardFieldInputInstance, isBoardFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { isExecutableNode, isInvocationNode } from 'features/nodes/types/invocation';
|
||||
import { omit, reduce } from 'lodash-es';
|
||||
import type { AnyInvocation, Graph } from 'services/api/types';
|
||||
@@ -7,11 +13,32 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const log = logger('workflows');
|
||||
|
||||
const getBoardField = (field: BoardFieldInputInstance, state: RootState): BoardField | undefined => {
|
||||
// Translate the UI value to the graph value. See note in BoardFieldInputComponent for more info.
|
||||
const { value } = field;
|
||||
|
||||
if (value === 'auto' || !value) {
|
||||
const autoAddBoardId = selectAutoAddBoardId(state);
|
||||
if (autoAddBoardId === 'none') {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
board_id: autoAddBoardId,
|
||||
};
|
||||
}
|
||||
|
||||
if (value === 'none') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a graph from the node editor state.
|
||||
*/
|
||||
export const buildNodesGraph = (nodesState: NodesState): Graph => {
|
||||
const { nodes, edges } = nodesState;
|
||||
export const buildNodesGraph = (state: RootState, templates: Templates): Graph => {
|
||||
const { nodes, edges } = selectNodesSlice(state);
|
||||
|
||||
// Exclude all batch nodes - we will handle these in the batch setup in a diff function
|
||||
const filteredNodes = nodes.filter(isInvocationNode).filter(isExecutableNode);
|
||||
@@ -21,11 +48,26 @@ export const buildNodesGraph = (nodesState: NodesState): Graph => {
|
||||
const { id, data } = node;
|
||||
const { type, inputs, isIntermediate } = data;
|
||||
|
||||
const nodeTemplate = templates[type];
|
||||
if (!nodeTemplate) {
|
||||
log.warn({ id, type }, 'Node template not found!');
|
||||
return nodesAccumulator;
|
||||
}
|
||||
|
||||
// Transform each node's inputs to simple key-value pairs
|
||||
const transformedInputs = reduce(
|
||||
inputs,
|
||||
(inputsAccumulator, input, name) => {
|
||||
inputsAccumulator[name] = input.value;
|
||||
const fieldTemplate = nodeTemplate.inputs[name];
|
||||
if (!fieldTemplate) {
|
||||
log.warn({ id, name }, 'Field template not found!');
|
||||
return inputsAccumulator;
|
||||
}
|
||||
if (isBoardFieldInputTemplate(fieldTemplate) && isBoardFieldInputInstance(input)) {
|
||||
inputsAccumulator[name] = getBoardField(input, state);
|
||||
} else {
|
||||
inputsAccumulator[name] = input.value;
|
||||
}
|
||||
|
||||
return inputsAccumulator;
|
||||
},
|
||||
|
||||
@@ -533,7 +533,7 @@ const buildBoardFieldInputTemplate: FieldInputTemplateBuilder<BoardFieldInputTem
|
||||
const template: BoardFieldInputTemplate = {
|
||||
...baseField,
|
||||
type: fieldType,
|
||||
default: schemaObject.default ?? undefined,
|
||||
default: schemaObject.default ?? 'auto',
|
||||
};
|
||||
|
||||
return template;
|
||||
|
||||
@@ -135,6 +135,9 @@ export const validateWorkflow = async (args: ValidateWorkflowArgs): Promise<Vali
|
||||
}
|
||||
}
|
||||
if (fieldTemplate.type.name === 'BoardField' && input.value && isBoardFieldInputInstance(input)) {
|
||||
if (input.value === 'none' || input.value === 'auto') {
|
||||
continue;
|
||||
}
|
||||
const hasAccess = await checkBoardAccess(input.value.board_id);
|
||||
if (!hasAccess) {
|
||||
const message = t('nodes.boardAccessError', { board_id: input.value.board_id });
|
||||
|
||||
Reference in New Issue
Block a user