refactor(ui): split workflow library state into separate slice

Has no business being in the workflow state slice.
This commit is contained in:
psychedelicious
2025-03-11 19:07:48 +10:00
parent 9ec4d968aa
commit 1756d885f6
11 changed files with 234 additions and 148 deletions

View File

@@ -16,10 +16,17 @@ import { $openAPISchemaUrl } from 'app/store/nanostores/openAPISchemaUrl';
import { $projectId, $projectName, $projectUrl } from 'app/store/nanostores/projectId';
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
import { $store } from 'app/store/nanostores/store';
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
import { createStore } from 'app/store/store';
import type { PartialAppConfig } from 'app/types/invokeai';
import Loading from 'common/components/Loading/Loading';
import type {
WorkflowTagCategory} from 'features/nodes/store/workflowLibrarySlice';
import {
$workflowLibraryCategoriesOptions,
$workflowLibraryTagCategoriesOptions,
DEFAULT_WORKFLOW_LIBRARY_CATEGORIES,
DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES
} from 'features/nodes/store/workflowLibrarySlice';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import type { PropsWithChildren, ReactNode } from 'react';
import React, { lazy, memo, useEffect, useLayoutEffect, useMemo } from 'react';
@@ -48,6 +55,7 @@ interface Props extends PropsWithChildren {
isDebugging?: boolean;
logo?: ReactNode;
workflowCategories?: WorkflowCategory[];
workflowTagCategories?: WorkflowTagCategory[];
loggingOverrides?: LoggingOverrides;
}
@@ -68,6 +76,7 @@ const InvokeAIUI = ({
isDebugging = false,
logo,
workflowCategories,
workflowTagCategories,
loggingOverrides,
}: Props) => {
useLayoutEffect(() => {
@@ -195,14 +204,24 @@ const InvokeAIUI = ({
useEffect(() => {
if (workflowCategories) {
$workflowCategories.set(workflowCategories);
$workflowLibraryCategoriesOptions.set(workflowCategories);
}
return () => {
$workflowCategories.set([]);
$workflowLibraryCategoriesOptions.set(DEFAULT_WORKFLOW_LIBRARY_CATEGORIES);
};
}, [workflowCategories]);
useEffect(() => {
if (workflowTagCategories) {
$workflowLibraryTagCategoriesOptions.set(workflowTagCategories);
}
return () => {
$workflowLibraryTagCategoriesOptions.set(DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES);
};
}, [workflowTagCategories]);
useEffect(() => {
if (socketOptions) {
$socketOptions.set(socketOptions);

View File

@@ -1,4 +0,0 @@
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import { atom } from 'nanostores';
export const $workflowCategories = atom<WorkflowCategory[]>(['user', 'default']);

View File

@@ -19,6 +19,7 @@ import { galleryPersistConfig, gallerySlice } from 'features/gallery/store/galle
import { hrfPersistConfig, hrfSlice } from 'features/hrf/store/hrfSlice';
import { modelManagerV2PersistConfig, modelManagerV2Slice } from 'features/modelManagerV2/store/modelManagerV2Slice';
import { nodesPersistConfig, nodesSlice, nodesUndoableConfig } from 'features/nodes/store/nodesSlice';
import { workflowLibraryPersistConfig, workflowLibrarySlice } from 'features/nodes/store/workflowLibrarySlice';
import { workflowSettingsPersistConfig, workflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice';
import { workflowPersistConfig, workflowSlice } from 'features/nodes/store/workflowSlice';
import { upscalePersistConfig, upscaleSlice } from 'features/parameters/store/upscaleSlice';
@@ -68,6 +69,7 @@ const allReducers = {
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
[canvasStagingAreaSlice.name]: canvasStagingAreaSlice.reducer,
[lorasSlice.name]: lorasSlice.reducer,
[workflowLibrarySlice.name]: workflowLibrarySlice.reducer,
};
const rootReducer = combineReducers(allReducers);
@@ -113,6 +115,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig,
[canvasStagingAreaPersistConfig.name]: canvasStagingAreaPersistConfig,
[lorasPersistConfig.name]: lorasPersistConfig,
[workflowLibraryPersistConfig.name]: workflowLibraryPersistConfig,
};
const unserialize: UnserializeFunction = (data, key) => {

View File

@@ -1,16 +1,17 @@
import type { ButtonProps, CheckboxProps } from '@invoke-ai/ui-library';
import { Button, Checkbox, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { WORKFLOW_TAGS, type WorkflowTag } from 'features/nodes/store/types';
import type { WorkflowTagCategory } from 'features/nodes/store/workflowLibrarySlice';
import {
selectWorkflowLibrarySelectedTags,
selectWorkflowSelectedCategories,
workflowSelectedCategoriesChanged,
workflowSelectedTagsRese,
workflowSelectedTagToggled,
} from 'features/nodes/store/workflowSlice';
$workflowLibraryCategoriesOptions,
$workflowLibraryTagCategoriesOptions,
selectWorkflowLibraryCategories,
selectWorkflowLibraryTags,
workflowLibraryCategoriesChanged,
workflowLibraryTagsReset,
workflowLibraryTagToggled,
} from 'features/nodes/store/workflowLibrarySlice';
import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton';
import { UploadWorkflowButton } from 'features/workflowLibrary/components/UploadWorkflowButton';
@@ -24,28 +25,29 @@ import type { S } from 'services/api/types';
export const WorkflowLibrarySideNav = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const categories = useAppSelector(selectWorkflowSelectedCategories);
const categoryOptions = useStore($workflowCategories);
const selectedTags = useAppSelector(selectWorkflowLibrarySelectedTags);
const categories = useAppSelector(selectWorkflowLibraryCategories);
const categoryOptions = useStore($workflowLibraryCategoriesOptions);
const tags = useAppSelector(selectWorkflowLibraryTags);
const tagCategoryOptions = useStore($workflowLibraryTagCategoriesOptions);
const selectYourWorkflows = useCallback(() => {
dispatch(workflowSelectedCategoriesChanged(categoryOptions.includes('project') ? ['user', 'project'] : ['user']));
dispatch(workflowLibraryCategoriesChanged(categoryOptions.includes('project') ? ['user', 'project'] : ['user']));
}, [categoryOptions, dispatch]);
const selectPrivateWorkflows = useCallback(() => {
dispatch(workflowSelectedCategoriesChanged(['user']));
dispatch(workflowLibraryCategoriesChanged(['user']));
}, [dispatch]);
const selectSharedWorkflows = useCallback(() => {
dispatch(workflowSelectedCategoriesChanged(['project']));
dispatch(workflowLibraryCategoriesChanged(['project']));
}, [dispatch]);
const selectDefaultWorkflows = useCallback(() => {
dispatch(workflowSelectedCategoriesChanged(['default']));
dispatch(workflowLibraryCategoriesChanged(['default']));
}, [dispatch]);
const resetTags = useCallback(() => {
dispatch(workflowSelectedTagsRese());
dispatch(workflowLibraryTagsReset());
}, [dispatch]);
const isYourWorkflowsSelected = useMemo(() => {
@@ -116,7 +118,7 @@ export const WorkflowLibrarySideNav = () => {
<Collapse in={isDefaultWorkflowsExclusivelySelected}>
<Flex flexDir="column" gap={2} pl={4} py={2} overflow="hidden" h="100%" minH={0}>
<Button
isDisabled={!isDefaultWorkflowsExclusivelySelected || selectedTags.length === 0}
isDisabled={!isDefaultWorkflowsExclusivelySelected || tags.length === 0}
onClick={resetTags}
size="sm"
variant="link"
@@ -130,9 +132,9 @@ export const WorkflowLibrarySideNav = () => {
{t('workflows.resetTags')}
</Button>
<Flex flexDir="column" gap={2} overflow="auto">
{WORKFLOW_TAGS.map((tagCategory) => (
{tagCategoryOptions.map((tagCategory) => (
<TagCategory
key={tagCategory.category}
key={tagCategory.categoryTKey}
tagCategory={tagCategory}
isDisabled={!isDefaultWorkflowsExclusivelySelected}
/>
@@ -218,40 +220,39 @@ const CategoryButton = memo(({ isSelected, ...rest }: ButtonProps & { isSelected
});
CategoryButton.displayName = 'NavButton';
const TagCategory = memo(
({ tagCategory, isDisabled }: { tagCategory: (typeof WORKFLOW_TAGS)[number]; isDisabled: boolean }) => {
const { count } = useGetCountsQuery(
{ tags: [...tagCategory.tags], categories: ['default'] },
{ selectFromResult: ({ data }) => ({ count: data ?? 0 }) }
);
const TagCategory = memo(({ tagCategory, isDisabled }: { tagCategory: WorkflowTagCategory; isDisabled: boolean }) => {
const { t } = useTranslation();
const { count } = useGetCountsQuery(
{ tags: [...tagCategory.tags], categories: ['default'] },
{ selectFromResult: ({ data }) => ({ count: data ?? 0 }) }
);
if (count === 0) {
return null;
}
return (
<Flex flexDir="column" gap={2}>
<Text fontWeight="semibold" color="base.300" opacity={isDisabled ? 0.5 : 1} flexShrink={0}>
{tagCategory.category}
</Text>
<Flex flexDir="column" gap={2} pl={4}>
{tagCategory.tags.map((tag) => (
<TagCheckbox key={tag} tag={tag} isDisabled={isDisabled} />
))}
</Flex>
</Flex>
);
if (count === 0) {
return null;
}
);
return (
<Flex flexDir="column" gap={2}>
<Text fontWeight="semibold" color="base.300" opacity={isDisabled ? 0.5 : 1} flexShrink={0}>
{t(tagCategory.categoryTKey)}
</Text>
<Flex flexDir="column" gap={2} pl={4}>
{tagCategory.tags.map((tag) => (
<TagCheckbox key={tag} tag={tag} isDisabled={isDisabled} />
))}
</Flex>
</Flex>
);
});
TagCategory.displayName = 'TagCategory';
const TagCheckbox = memo(({ tag, ...rest }: CheckboxProps & { tag: WorkflowTag }) => {
const TagCheckbox = memo(({ tag, ...rest }: CheckboxProps & { tag: string }) => {
const dispatch = useAppDispatch();
const selectedTags = useAppSelector(selectWorkflowLibrarySelectedTags);
const selectedTags = useAppSelector(selectWorkflowLibraryTags);
const isSelected = selectedTags.includes(tag);
const onChange = useCallback(() => {
dispatch(workflowSelectedTagToggled(tag));
dispatch(workflowLibraryTagToggled(tag));
}, [dispatch, tag]);
const { count } = useGetCountsQuery(

View File

@@ -3,15 +3,15 @@ import { EMPTY_ARRAY } from 'app/store/constants';
import { useAppSelector } from 'app/store/storeHooks';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import {
selectWorkflowLibrarySelectedTags,
selectWorkflowOrderBy,
selectWorkflowOrderDirection,
selectWorkflowSearchTerm,
selectWorkflowSelectedCategories,
} from 'features/nodes/store/workflowSlice';
selectWorkflowLibraryCategories,
selectWorkflowLibraryDirection,
selectWorkflowLibraryHasSearchTerm,
selectWorkflowLibraryOrderBy,
selectWorkflowLibrarySearchTerm,
selectWorkflowLibraryTags,
} from 'features/nodes/store/workflowLibrarySlice';
import { memo, useCallback, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import type { useListWorkflowsQuery } from 'services/api/endpoints/workflows';
import { useListWorkflowsInfiniteInfiniteQuery } from 'services/api/endpoints/workflows';
import type { S } from 'services/api/types';
import { useDebounce } from 'use-debounce';
@@ -21,11 +21,11 @@ import { WorkflowListItem } from './WorkflowListItem';
const PER_PAGE = 30;
const useInfiniteQueryAry = () => {
const categories = useAppSelector(selectWorkflowSelectedCategories);
const orderBy = useAppSelector(selectWorkflowOrderBy);
const direction = useAppSelector(selectWorkflowOrderDirection);
const query = useAppSelector(selectWorkflowSearchTerm);
const tags = useAppSelector(selectWorkflowLibrarySelectedTags);
const categories = useAppSelector(selectWorkflowLibraryCategories);
const orderBy = useAppSelector(selectWorkflowLibraryOrderBy);
const direction = useAppSelector(selectWorkflowLibraryDirection);
const query = useAppSelector(selectWorkflowLibrarySearchTerm);
const tags = useAppSelector(selectWorkflowLibraryTags);
const [debouncedQuery] = useDebounce(query, 500);
const queryArg = useMemo(() => {
@@ -37,7 +37,7 @@ const useInfiniteQueryAry = () => {
categories,
query: debouncedQuery,
tags: categories.length === 1 && categories.includes('default') ? tags : [],
} satisfies Parameters<typeof useListWorkflowsQuery>[0];
} satisfies Parameters<typeof useListWorkflowsInfiniteInfiniteQuery>[0];
}, [orderBy, direction, categories, debouncedQuery, tags]);
return queryArg;
@@ -53,8 +53,6 @@ const queryOptions = {
} satisfies Parameters<typeof useListWorkflowsInfiniteInfiniteQuery>[1];
export const WorkflowList = () => {
const searchTerm = useAppSelector(selectWorkflowSearchTerm);
const { t } = useTranslation();
const queryArg = useInfiniteQueryAry();
const { items, isFetching, isLoading, fetchNextPage, hasNextPage } = useListWorkflowsInfiniteInfiniteQuery(
queryArg,
@@ -70,14 +68,7 @@ export const WorkflowList = () => {
}
if (items.length === 0) {
return (
<IAINoContentFallback
fontSize="sm"
py={4}
label={searchTerm ? t('nodes.noMatchingWorkflows') : t('nodes.noWorkflows')}
icon={null}
/>
);
return <NoItems />;
}
return (
@@ -90,6 +81,20 @@ export const WorkflowList = () => {
);
};
const NoItems = memo(() => {
const { t } = useTranslation();
const hasSearchTerm = useAppSelector(selectWorkflowLibraryHasSearchTerm);
return (
<IAINoContentFallback
fontSize="sm"
py={4}
label={hasSearchTerm ? t('nodes.noMatchingWorkflows') : t('nodes.noWorkflows')}
icon={null}
/>
);
});
NoItems.displayName = 'NoItems';
const WorkflowListContent = memo(
({
items,

View File

@@ -1,6 +1,9 @@
import { Flex, IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectWorkflowSearchTerm, workflowSearchTermChanged } from 'features/nodes/store/workflowSlice';
import {
selectWorkflowLibrarySearchTerm,
workflowLibrarySearchTermChanged,
} from 'features/nodes/store/workflowLibrarySlice';
import type { ChangeEvent, KeyboardEvent, RefObject } from 'react';
import { memo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,18 +11,18 @@ import { PiXBold } from 'react-icons/pi';
export const WorkflowSearch = memo(({ searchInputRef }: { searchInputRef: RefObject<HTMLInputElement> }) => {
const dispatch = useAppDispatch();
const searchTerm = useAppSelector(selectWorkflowSearchTerm);
const searchTerm = useAppSelector(selectWorkflowLibrarySearchTerm);
const { t } = useTranslation();
const handleWorkflowSearch = useCallback(
(newSearchTerm: string) => {
dispatch(workflowSearchTermChanged(newSearchTerm));
dispatch(workflowLibrarySearchTermChanged(newSearchTerm));
},
[dispatch]
);
const clearWorkflowSearch = useCallback(() => {
dispatch(workflowSearchTermChanged(''));
dispatch(workflowLibrarySearchTermChanged(''));
}, [dispatch]);
const handleKeydown = useCallback(

View File

@@ -1,11 +1,11 @@
import { Flex, FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import {
selectWorkflowOrderBy,
selectWorkflowOrderDirection,
workflowOrderByChanged,
workflowOrderDirectionChanged,
} from 'features/nodes/store/workflowSlice';
selectWorkflowLibraryDirection,
selectWorkflowLibraryOrderBy,
workflowLibraryDirectionChanged,
workflowLibraryOrderByChanged,
} from 'features/nodes/store/workflowLibrarySlice';
import type { ChangeEvent } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -22,8 +22,8 @@ const isDirection = (v: unknown): v is Direction => zDirection.safeParse(v).succ
export const WorkflowSortControl = () => {
const { t } = useTranslation();
const orderBy = useAppSelector(selectWorkflowOrderBy);
const direction = useAppSelector(selectWorkflowOrderDirection);
const orderBy = useAppSelector(selectWorkflowLibraryOrderBy);
const direction = useAppSelector(selectWorkflowLibraryDirection);
const ORDER_BY_LABELS = useMemo(
() => ({
@@ -50,7 +50,7 @@ export const WorkflowSortControl = () => {
if (!isOrderBy(e.target.value)) {
return;
}
dispatch(workflowOrderByChanged(e.target.value));
dispatch(workflowLibraryOrderByChanged(e.target.value));
},
[dispatch]
);
@@ -60,7 +60,7 @@ export const WorkflowSortControl = () => {
if (!isDirection(e.target.value)) {
return;
}
dispatch(workflowOrderDirectionChanged(e.target.value));
dispatch(workflowLibraryDirectionChanged(e.target.value));
},
[dispatch]
);

View File

@@ -1,8 +1,7 @@
import type { HandleType } from '@xyflow/react';
import type { FieldInputTemplate, FieldOutputTemplate, StatefulFieldValue } from 'features/nodes/types/field';
import type { AnyEdge, AnyNode, InvocationTemplate, NodeExecutionState } from 'features/nodes/types/invocation';
import type { WorkflowCategory, WorkflowV3 } from 'features/nodes/types/workflow';
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
export type Templates = Record<string, InvocationTemplate>;
export type NodeExecutionStates = Record<string, NodeExecutionState | undefined>;
@@ -22,22 +21,9 @@ export type NodesState = {
export type WorkflowMode = 'edit' | 'view';
export const WORKFLOW_TAGS = [
{ category: 'Industry', tags: ['Architecture', 'Fashion', 'Game Dev', 'Food'] },
{ category: 'Common Tasks', tags: ['Upscaling', 'Text to Image', 'Image to Image'] },
{ category: 'Model Architecture', tags: ['SD1.5', 'SDXL', 'Bria', 'FLUX'] },
{ category: 'Tech Showcase', tags: ['Control', 'Reference Image'] },
] as const;
export type WorkflowTag = (typeof WORKFLOW_TAGS)[number]['tags'][number];
export type WorkflowsState = Omit<WorkflowV3, 'nodes' | 'edges'> & {
_version: 1;
isTouched: boolean;
mode: WorkflowMode;
selectedTags: WorkflowTag[];
selectedCategories: WorkflowCategory[];
searchTerm: string;
orderBy?: WorkflowRecordOrderBy;
orderDirection: SQLiteDirection;
formFieldInitialValues: Record<string, StatefulFieldValue>;
};

View File

@@ -0,0 +1,98 @@
import type { PayloadAction, Selector } from '@reduxjs/toolkit';
import { createSelector, createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import { atom } from 'nanostores';
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
type WorkflowLibraryState = {
searchTerm: string;
orderBy: WorkflowRecordOrderBy;
direction: SQLiteDirection;
tags: string[];
categories: WorkflowCategory[];
};
const initialWorkflowLibraryState: WorkflowLibraryState = {
searchTerm: '',
orderBy: 'opened_at',
direction: 'DESC',
tags: [],
categories: ['user'],
};
export const workflowLibrarySlice = createSlice({
name: 'workflowLibrary',
initialState: initialWorkflowLibraryState,
reducers: {
workflowLibrarySearchTermChanged: (state, action: PayloadAction<string>) => {
state.searchTerm = action.payload;
},
workflowLibraryOrderByChanged: (state, action: PayloadAction<WorkflowRecordOrderBy>) => {
state.orderBy = action.payload;
},
workflowLibraryDirectionChanged: (state, action: PayloadAction<SQLiteDirection>) => {
state.direction = action.payload;
},
workflowLibraryCategoriesChanged: (state, action: PayloadAction<WorkflowCategory[]>) => {
state.categories = action.payload;
state.searchTerm = '';
},
workflowLibraryTagToggled: (state, action: PayloadAction<string>) => {
const tag = action.payload;
const tags = state.tags;
if (tags.includes(tag)) {
state.tags = tags.filter((t) => t !== tag);
} else {
state.tags = [...tags, tag];
}
},
workflowLibraryTagsReset: (state) => {
state.tags = [];
},
},
});
export const {
workflowLibrarySearchTermChanged,
workflowLibraryOrderByChanged,
workflowLibraryDirectionChanged,
workflowLibraryCategoriesChanged,
workflowLibraryTagToggled,
workflowLibraryTagsReset,
} = workflowLibrarySlice.actions;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const migrateWorkflowLibraryState = (state: any): any => state;
export const workflowLibraryPersistConfig: PersistConfig<WorkflowLibraryState> = {
name: workflowLibrarySlice.name,
initialState: initialWorkflowLibraryState,
migrate: migrateWorkflowLibraryState,
persistDenylist: [],
};
export const selectWorkflowLibrarySlice = (state: RootState) => state.workflowLibrary;
const createWorkflowLibrarySelector = <T>(selector: Selector<WorkflowLibraryState, T>) =>
createSelector(selectWorkflowLibrarySlice, selector);
export const selectWorkflowLibrarySearchTerm = createWorkflowLibrarySelector(({ searchTerm }) => searchTerm);
export const selectWorkflowLibraryHasSearchTerm = createWorkflowLibrarySelector(({ searchTerm }) => !!searchTerm);
export const selectWorkflowLibraryOrderBy = createWorkflowLibrarySelector(({ orderBy }) => orderBy);
export const selectWorkflowLibraryDirection = createWorkflowLibrarySelector(({ direction }) => direction);
export const selectWorkflowLibraryTags = createWorkflowLibrarySelector(({ tags }) => tags);
export const selectWorkflowLibraryCategories = createWorkflowLibrarySelector(({ categories }) => categories);
export const DEFAULT_WORKFLOW_LIBRARY_CATEGORIES = ['user', 'default'] satisfies WorkflowCategory[];
export const $workflowLibraryCategoriesOptions = atom<WorkflowCategory[]>(DEFAULT_WORKFLOW_LIBRARY_CATEGORIES);
export type WorkflowTagCategory = { categoryTKey: string; tags: string[] };
export const DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES: WorkflowTagCategory[] = [
{ categoryTKey: 'Industry', tags: ['Architecture', 'Fashion', 'Game Dev', 'Food'] },
{ categoryTKey: 'Common Tasks', tags: ['Upscaling', 'Text to Image', 'Image to Image'] },
{ categoryTKey: 'Model Architecture', tags: ['SD1.5', 'SDXL', 'Bria', 'FLUX'] },
{ categoryTKey: 'Tech Showcase', tags: ['Control', 'Reference Image'] },
];
export const $workflowLibraryTagCategoriesOptions = atom<WorkflowTagCategory[]>(
DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES
);

View File

@@ -15,7 +15,6 @@ import type {
NodesState,
WorkflowMode,
WorkflowsState as WorkflowState,
WorkflowTag,
} from 'features/nodes/store/types';
import type { FieldIdentifier, StatefulFieldValue } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
@@ -39,8 +38,9 @@ import {
isTextElement,
} from 'features/nodes/types/workflow';
import { isEqual } from 'lodash-es';
import { atom } from 'nanostores';
import { useMemo } from 'react';
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
import type { WorkflowRecordOrderBy } from 'services/api/types';
import { selectNodesSlice } from './selectors';
@@ -83,11 +83,6 @@ const initialWorkflowState: WorkflowState = {
isTouched: false,
mode: 'view',
formFieldInitialValues: {},
searchTerm: '',
orderBy: 'opened_at', // initial value is decided in component
orderDirection: 'DESC',
selectedTags: [],
selectedCategories: ['user'],
...getBlankWorkflow(),
};
@@ -98,19 +93,6 @@ export const workflowSlice = createSlice({
workflowModeChanged: (state, action: PayloadAction<WorkflowMode>) => {
state.mode = action.payload;
},
workflowSearchTermChanged: (state, action: PayloadAction<string>) => {
state.searchTerm = action.payload;
},
workflowOrderByChanged: (state, action: PayloadAction<WorkflowRecordOrderBy>) => {
state.orderBy = action.payload;
},
workflowOrderDirectionChanged: (state, action: PayloadAction<SQLiteDirection>) => {
state.orderDirection = action.payload;
},
workflowSelectedCategoriesChanged: (state, action: PayloadAction<WorkflowCategory[]>) => {
state.selectedCategories = action.payload;
state.searchTerm = '';
},
workflowNameChanged: (state, action: PayloadAction<string>) => {
state.name = action.payload;
state.isTouched = true;
@@ -150,18 +132,6 @@ export const workflowSlice = createSlice({
workflowSaved: (state) => {
state.isTouched = false;
},
workflowSelectedTagToggled: (state, action: PayloadAction<WorkflowTag>) => {
const tag = action.payload;
const tags = state.selectedTags;
if (tags.includes(tag)) {
state.selectedTags = tags.filter((t) => t !== tag);
} else {
state.selectedTags = [...tags, tag];
}
},
workflowSelectedTagsRese: (state) => {
state.selectedTags = [];
},
formReset: (state) => {
const rootElement = buildContainer('column', []);
state.form = {
@@ -314,12 +284,6 @@ export const {
workflowContactChanged,
workflowIDChanged,
workflowSaved,
workflowSearchTermChanged,
workflowOrderByChanged,
workflowOrderDirectionChanged,
workflowSelectedCategoriesChanged,
workflowSelectedTagToggled,
workflowSelectedTagsRese,
formReset,
formElementAdded,
formElementRemoved,
@@ -382,12 +346,7 @@ export const selectWorkflowName = createWorkflowSelector((workflow) => workflow.
export const selectWorkflowId = createWorkflowSelector((workflow) => workflow.id);
export const selectWorkflowMode = createWorkflowSelector((workflow) => workflow.mode);
export const selectWorkflowIsTouched = createWorkflowSelector((workflow) => workflow.isTouched);
export const selectWorkflowSearchTerm = createWorkflowSelector((workflow) => workflow.searchTerm);
export const selectWorkflowOrderBy = createWorkflowSelector((workflow) => workflow.orderBy);
export const selectWorkflowOrderDirection = createWorkflowSelector((workflow) => workflow.orderDirection);
export const selectWorkflowSelectedCategories = createWorkflowSelector((workflow) => workflow.selectedCategories);
export const selectWorkflowDescription = createWorkflowSelector((workflow) => workflow.description);
export const selectWorkflowLibrarySelectedTags = createWorkflowSelector((workflow) => workflow.selectedTags);
export const selectWorkflowForm = createWorkflowSelector((workflow) => workflow.form);
export const selectCleanEditor = createSelector([selectNodesSlice, selectWorkflowSlice], (nodes, workflow) => {
@@ -420,3 +379,19 @@ export const useElement = (id: string): FormElement | undefined => {
const element = useAppSelector(selector);
return element;
};
export const DEFAULT_WORKFLOW_CATEGORIES = ['user', 'default'] satisfies WorkflowCategory[];
export const $workflowCategories = atom<WorkflowCategory[]>(DEFAULT_WORKFLOW_CATEGORIES);
export const $selectedWorkflowCategories = atom<WorkflowCategory[]>(['user']);
export const DEFAULT_WORKFLOW_TAG_CATEGORIES = {
Industry: ['Architecture', 'Fashion', 'Game Dev', 'Food'],
'Common Tasks': ['Upscaling', 'Text to Image', 'Image to Image'],
'Model Architecture': ['SD1.5', 'SDXL', 'Bria', 'FLUX'],
'Tech Showcase': ['Control', 'Reference Image'],
} satisfies Record<string, string[]>;
export const $workflowTagCategories = atom<Record<string, string[]>>(DEFAULT_WORKFLOW_TAG_CATEGORIES);
export const $selectedWorkflowTags = atom<string[]>([]);
export const $workflowLibarySearchTerm = atom<string>('');
export const $workflowLibraryOrderBy = atom<WorkflowRecordOrderBy>('opened_at');

View File

@@ -12,9 +12,9 @@ import {
Input,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
import { deepClone } from 'common/util/deepClone';
import { $workflowLibraryCategoriesOptions } from 'features/nodes/store/workflowLibrarySlice';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
import { isDraftWorkflow, useCreateLibraryWorkflow } from 'features/workflowLibrary/hooks/useCreateNewWorkflow';
import { t } from 'i18next';
@@ -83,7 +83,7 @@ export const SaveWorkflowAsDialog = () => {
};
const Content = memo(({ workflow, cancelRef }: { workflow: WorkflowV3; cancelRef: RefObject<HTMLButtonElement> }) => {
const workflowCategories = useStore($workflowCategories);
const workflowCategories = useStore($workflowLibraryCategoriesOptions);
const [name, setName] = useState(() => {
if (workflow) {
return getInitialName(workflow);