mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
refactor(ui): split workflow library state into separate slice
Has no business being in the workflow state slice.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { WorkflowCategory } from 'features/nodes/types/workflow';
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export const $workflowCategories = atom<WorkflowCategory[]>(['user', 'default']);
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user