diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 3f66ae4a9e..d08e46ec99 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -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); diff --git a/invokeai/frontend/web/src/app/store/nanostores/workflowCategories.ts b/invokeai/frontend/web/src/app/store/nanostores/workflowCategories.ts deleted file mode 100644 index e0d6107129..0000000000 --- a/invokeai/frontend/web/src/app/store/nanostores/workflowCategories.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { WorkflowCategory } from 'features/nodes/types/workflow'; -import { atom } from 'nanostores'; - -export const $workflowCategories = atom(['user', 'default']); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index a36300cca9..a2028b49e1 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -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) => { diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx index 0f2f9d6692..9abeebe9cf 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx @@ -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 = () => { - {WORKFLOW_TAGS.map((tagCategory) => ( + {tagCategoryOptions.map((tagCategory) => ( @@ -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 ( - - - {tagCategory.category} - - - {tagCategory.tags.map((tag) => ( - - ))} - - - ); + if (count === 0) { + return null; } -); + + return ( + + + {t(tagCategory.categoryTKey)} + + + {tagCategory.tags.map((tag) => ( + + ))} + + + ); +}); 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( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx index 99c578679c..049bb0b8c8 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx @@ -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[0]; + } satisfies Parameters[0]; }, [orderBy, direction, categories, debouncedQuery, tags]); return queryArg; @@ -53,8 +53,6 @@ const queryOptions = { } satisfies Parameters[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 ( - - ); + return ; } return ( @@ -90,6 +81,20 @@ export const WorkflowList = () => { ); }; +const NoItems = memo(() => { + const { t } = useTranslation(); + const hasSearchTerm = useAppSelector(selectWorkflowLibraryHasSearchTerm); + + return ( + + ); +}); +NoItems.displayName = 'NoItems'; const WorkflowListContent = memo( ({ items, diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSearch.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSearch.tsx index 599f7e1163..058fc12daa 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSearch.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSearch.tsx @@ -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 }) => { 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( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSortControl.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSortControl.tsx index 9a1606aa8f..1249b43991 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSortControl.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSortControl.tsx @@ -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] ); diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index bbafe0eebb..0d22306cef 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -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; export type NodeExecutionStates = Record; @@ -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 & { _version: 1; isTouched: boolean; mode: WorkflowMode; - selectedTags: WorkflowTag[]; - selectedCategories: WorkflowCategory[]; - searchTerm: string; - orderBy?: WorkflowRecordOrderBy; - orderDirection: SQLiteDirection; formFieldInitialValues: Record; }; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts new file mode 100644 index 0000000000..838895da90 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/workflowLibrarySlice.ts @@ -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) => { + state.searchTerm = action.payload; + }, + workflowLibraryOrderByChanged: (state, action: PayloadAction) => { + state.orderBy = action.payload; + }, + workflowLibraryDirectionChanged: (state, action: PayloadAction) => { + state.direction = action.payload; + }, + workflowLibraryCategoriesChanged: (state, action: PayloadAction) => { + state.categories = action.payload; + state.searchTerm = ''; + }, + workflowLibraryTagToggled: (state, action: PayloadAction) => { + 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 = { + name: workflowLibrarySlice.name, + initialState: initialWorkflowLibraryState, + migrate: migrateWorkflowLibraryState, + persistDenylist: [], +}; + +export const selectWorkflowLibrarySlice = (state: RootState) => state.workflowLibrary; +const createWorkflowLibrarySelector = (selector: Selector) => + 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(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( + DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES +); diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index fda9049124..04c88fd30b 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -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) => { state.mode = action.payload; }, - workflowSearchTermChanged: (state, action: PayloadAction) => { - state.searchTerm = action.payload; - }, - workflowOrderByChanged: (state, action: PayloadAction) => { - state.orderBy = action.payload; - }, - workflowOrderDirectionChanged: (state, action: PayloadAction) => { - state.orderDirection = action.payload; - }, - workflowSelectedCategoriesChanged: (state, action: PayloadAction) => { - state.selectedCategories = action.payload; - state.searchTerm = ''; - }, workflowNameChanged: (state, action: PayloadAction) => { state.name = action.payload; state.isTouched = true; @@ -150,18 +132,6 @@ export const workflowSlice = createSlice({ workflowSaved: (state) => { state.isTouched = false; }, - workflowSelectedTagToggled: (state, action: PayloadAction) => { - 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(DEFAULT_WORKFLOW_CATEGORIES); +export const $selectedWorkflowCategories = atom(['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; +export const $workflowTagCategories = atom>(DEFAULT_WORKFLOW_TAG_CATEGORIES); +export const $selectedWorkflowTags = atom([]); + +export const $workflowLibarySearchTerm = atom(''); +export const $workflowLibraryOrderBy = atom('opened_at'); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog.tsx index 7e57a00264..1bdee73d7d 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog.tsx @@ -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 }) => { - const workflowCategories = useStore($workflowCategories); + const workflowCategories = useStore($workflowLibraryCategoriesOptions); const [name, setName] = useState(() => { if (workflow) { return getInitialName(workflow);