mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): on first load, if the selected library view has no workflows, switch to the first view that has workflows
This commit is contained in:
@@ -8,9 +8,18 @@ import {
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal';
|
||||
import { memo } from 'react';
|
||||
import {
|
||||
$workflowLibraryCategoriesOptions,
|
||||
selectWorkflowLibraryView,
|
||||
workflowLibraryViewChanged,
|
||||
} from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetCountsByCategoryQuery } from 'services/api/endpoints/workflows';
|
||||
|
||||
import { WorkflowLibrarySideNav } from './WorkflowLibrarySideNav';
|
||||
import { WorkflowLibraryTopNav } from './WorkflowLibraryTopNav';
|
||||
@@ -19,6 +28,7 @@ import { WorkflowList } from './WorkflowList';
|
||||
export const WorkflowLibraryModal = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const workflowLibraryModal = useWorkflowLibraryModal();
|
||||
const didSync = useSyncInitialWorkflowLibraryCategories();
|
||||
return (
|
||||
<Modal isOpen={workflowLibraryModal.isOpen} onClose={workflowLibraryModal.close} isCentered>
|
||||
<ModalOverlay />
|
||||
@@ -31,14 +41,17 @@ export const WorkflowLibraryModal = memo(() => {
|
||||
<ModalHeader>{t('workflows.workflowLibrary')}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6}>
|
||||
<Flex gap={4} h="100%">
|
||||
<WorkflowLibrarySideNav />
|
||||
<Divider orientation="vertical" />
|
||||
<Flex flexDir="column" flex={1} gap={4}>
|
||||
<WorkflowLibraryTopNav />
|
||||
<WorkflowList />
|
||||
{didSync && (
|
||||
<Flex gap={4} h="100%">
|
||||
<WorkflowLibrarySideNav />
|
||||
<Divider orientation="vertical" />
|
||||
<Flex flexDir="column" flex={1} gap={4}>
|
||||
<WorkflowLibraryTopNav />
|
||||
<WorkflowList />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{!didSync && <IAINoContentFallback label={t('workflows.loading')} icon={null} />}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
@@ -46,3 +59,84 @@ export const WorkflowLibraryModal = memo(() => {
|
||||
});
|
||||
|
||||
WorkflowLibraryModal.displayName = 'WorkflowLibraryModal';
|
||||
|
||||
/**
|
||||
* On first app load, if the user's selected view has no workflows, switches to the next available view.
|
||||
*/
|
||||
const useSyncInitialWorkflowLibraryCategories = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const view = useAppSelector(selectWorkflowLibraryView);
|
||||
const categoryOptions = useStore($workflowLibraryCategoriesOptions);
|
||||
const [didSync, setDidSync] = useState(false);
|
||||
const recentWorkflowsCountQueryArg = useMemo(
|
||||
() =>
|
||||
({
|
||||
categories: ['user', 'project', 'default'],
|
||||
has_been_opened: true,
|
||||
}) satisfies Parameters<typeof useGetCountsByCategoryQuery>[0],
|
||||
[]
|
||||
);
|
||||
const yourWorkflowsCountQueryArg = useMemo(
|
||||
() =>
|
||||
({
|
||||
categories: ['user', 'project'],
|
||||
}) satisfies Parameters<typeof useGetCountsByCategoryQuery>[0],
|
||||
[]
|
||||
);
|
||||
const queryOptions = useMemo(
|
||||
() =>
|
||||
({
|
||||
selectFromResult: ({ data, isLoading }) => {
|
||||
if (!data) {
|
||||
return { count: 0, isLoading: true };
|
||||
}
|
||||
return {
|
||||
count: Object.values(data).reduce((acc, count) => acc + count, 0),
|
||||
isLoading,
|
||||
};
|
||||
},
|
||||
}) satisfies Parameters<typeof useGetCountsByCategoryQuery>[1],
|
||||
[]
|
||||
);
|
||||
|
||||
const { count: recentWorkflowsCount, isLoading: isLoadingRecentWorkflowsCount } = useGetCountsByCategoryQuery(
|
||||
recentWorkflowsCountQueryArg,
|
||||
queryOptions
|
||||
);
|
||||
const { count: yourWorkflowsCount, isLoading: isLoadingYourWorkflowsCount } = useGetCountsByCategoryQuery(
|
||||
yourWorkflowsCountQueryArg,
|
||||
queryOptions
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (didSync || isLoadingRecentWorkflowsCount || isLoadingYourWorkflowsCount) {
|
||||
return;
|
||||
}
|
||||
// If the user's selected view has no workflows, switch to the next available view
|
||||
if (recentWorkflowsCount === 0 && view === 'recent') {
|
||||
if (yourWorkflowsCount > 0) {
|
||||
dispatch(workflowLibraryViewChanged('yours'));
|
||||
} else {
|
||||
dispatch(workflowLibraryViewChanged('defaults'));
|
||||
}
|
||||
} else if (yourWorkflowsCount === 0 && (view === 'yours' || view === 'shared' || view === 'private')) {
|
||||
if (recentWorkflowsCount > 0) {
|
||||
dispatch(workflowLibraryViewChanged('recent'));
|
||||
} else {
|
||||
dispatch(workflowLibraryViewChanged('defaults'));
|
||||
}
|
||||
}
|
||||
setDidSync(true);
|
||||
}, [
|
||||
categoryOptions,
|
||||
didSync,
|
||||
dispatch,
|
||||
isLoadingRecentWorkflowsCount,
|
||||
isLoadingYourWorkflowsCount,
|
||||
recentWorkflowsCount,
|
||||
view,
|
||||
yourWorkflowsCount,
|
||||
]);
|
||||
|
||||
return didSync;
|
||||
};
|
||||
|
||||
@@ -2,18 +2,16 @@ 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 { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { WorkflowTagCategory } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import type { WorkflowLibraryView, WorkflowTagCategory } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import {
|
||||
$workflowLibraryCategoriesOptions,
|
||||
$workflowLibraryTagCategoriesOptions,
|
||||
$workflowLibraryTagOptions,
|
||||
selectWorkflowLibraryCategories,
|
||||
selectWorkflowLibraryShowOpenedWorkflowsOnly,
|
||||
selectWorkflowLibraryTags,
|
||||
workflowLibraryCategoriesChanged,
|
||||
workflowLibraryShowOpenedWorkflowsOnlyChanged,
|
||||
selectWorkflowLibrarySelectedTags,
|
||||
selectWorkflowLibraryView,
|
||||
workflowLibraryTagsReset,
|
||||
workflowLibraryTagToggled,
|
||||
workflowLibraryViewChanged,
|
||||
} from 'features/nodes/store/workflowLibrarySlice';
|
||||
import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton';
|
||||
@@ -27,135 +25,33 @@ import type { S } from 'services/api/types';
|
||||
|
||||
export const WorkflowLibrarySideNav = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const categories = useAppSelector(selectWorkflowLibraryCategories);
|
||||
const categoryOptions = useStore($workflowLibraryCategoriesOptions);
|
||||
const tags = useAppSelector(selectWorkflowLibraryTags);
|
||||
const showOpenedWorkflowsOnly = useAppSelector(selectWorkflowLibraryShowOpenedWorkflowsOnly);
|
||||
const tagCategoryOptions = useStore($workflowLibraryTagCategoriesOptions);
|
||||
|
||||
const selectYourWorkflows = useCallback(() => {
|
||||
dispatch(workflowLibraryCategoriesChanged(categoryOptions.includes('project') ? ['user', 'project'] : ['user']));
|
||||
dispatch(workflowLibraryShowOpenedWorkflowsOnlyChanged(false));
|
||||
}, [categoryOptions, dispatch]);
|
||||
|
||||
const selectPrivateWorkflows = useCallback(() => {
|
||||
dispatch(workflowLibraryCategoriesChanged(['user']));
|
||||
dispatch(workflowLibraryShowOpenedWorkflowsOnlyChanged(false));
|
||||
}, [dispatch]);
|
||||
|
||||
const selectSharedWorkflows = useCallback(() => {
|
||||
dispatch(workflowLibraryCategoriesChanged(['project']));
|
||||
dispatch(workflowLibraryShowOpenedWorkflowsOnlyChanged(false));
|
||||
}, [dispatch]);
|
||||
|
||||
const selectDefaultWorkflows = useCallback(() => {
|
||||
dispatch(workflowLibraryCategoriesChanged(['default']));
|
||||
dispatch(workflowLibraryShowOpenedWorkflowsOnlyChanged(false));
|
||||
}, [dispatch]);
|
||||
|
||||
const selectRecentWorkflows = useCallback(() => {
|
||||
dispatch(workflowLibraryCategoriesChanged(['default', 'user', 'project']));
|
||||
dispatch(workflowLibraryShowOpenedWorkflowsOnlyChanged(true));
|
||||
}, [dispatch]);
|
||||
|
||||
const resetTags = useCallback(() => {
|
||||
dispatch(workflowLibraryTagsReset());
|
||||
}, [dispatch]);
|
||||
|
||||
const isYourWorkflowsSelected = useMemo(() => {
|
||||
if (categoryOptions.includes('project')) {
|
||||
return categories.includes('user') && categories.includes('project') && !showOpenedWorkflowsOnly;
|
||||
} else {
|
||||
return categories.includes('user') && !showOpenedWorkflowsOnly;
|
||||
}
|
||||
}, [categoryOptions, categories, showOpenedWorkflowsOnly]);
|
||||
|
||||
const isPrivateWorkflowsExclusivelySelected = useMemo(() => {
|
||||
return categories.length === 1 && categories.includes('user') && !showOpenedWorkflowsOnly;
|
||||
}, [categories, showOpenedWorkflowsOnly]);
|
||||
|
||||
const isSharedWorkflowsExclusivelySelected = useMemo(() => {
|
||||
return categories.length === 1 && categories.includes('project') && !showOpenedWorkflowsOnly;
|
||||
}, [categories, showOpenedWorkflowsOnly]);
|
||||
|
||||
const isDefaultWorkflowsExclusivelySelected = useMemo(() => {
|
||||
return categories.length === 1 && categories.includes('default') && !showOpenedWorkflowsOnly;
|
||||
}, [categories, showOpenedWorkflowsOnly]);
|
||||
|
||||
const isRecentWorkflowsSelected = useMemo(() => {
|
||||
return categories.length === 3 && showOpenedWorkflowsOnly;
|
||||
}, [categories, showOpenedWorkflowsOnly]);
|
||||
const view = useAppSelector(selectWorkflowLibraryView);
|
||||
|
||||
return (
|
||||
<Flex h="full" minH={0} overflow="hidden" flexDir="column" w={64} gap={1}>
|
||||
<Flex flexDir="column" w="full" pb={2}>
|
||||
<CategoryButton isSelected={isRecentWorkflowsSelected} onClick={selectRecentWorkflows}>
|
||||
{t('workflows.recentlyOpened')}
|
||||
</CategoryButton>
|
||||
<WorkflowLibraryViewButton view="recent">{t('workflows.recentlyOpened')}</WorkflowLibraryViewButton>
|
||||
</Flex>
|
||||
<Flex flexDir="column" w="full" pb={2}>
|
||||
<CategoryButton isSelected={isYourWorkflowsSelected} onClick={selectYourWorkflows}>
|
||||
{t('workflows.yourWorkflows')}
|
||||
</CategoryButton>
|
||||
<WorkflowLibraryViewButton view="yours">{t('workflows.yourWorkflows')}</WorkflowLibraryViewButton>
|
||||
{categoryOptions.includes('project') && (
|
||||
<Collapse
|
||||
in={
|
||||
isYourWorkflowsSelected || isPrivateWorkflowsExclusivelySelected || isSharedWorkflowsExclusivelySelected
|
||||
}
|
||||
>
|
||||
<Collapse in={view === 'yours' || view === 'shared' || view === 'private'}>
|
||||
<Flex flexDir="column" gap={2} pl={4} pt={2}>
|
||||
<CategoryButton
|
||||
size="sm"
|
||||
onClick={selectPrivateWorkflows}
|
||||
isSelected={isPrivateWorkflowsExclusivelySelected}
|
||||
>
|
||||
<WorkflowLibraryViewButton size="sm" view="private">
|
||||
{t('workflows.private')}
|
||||
</CategoryButton>
|
||||
<CategoryButton
|
||||
size="sm"
|
||||
rightIcon={<PiUsersBold />}
|
||||
onClick={selectSharedWorkflows}
|
||||
isSelected={isSharedWorkflowsExclusivelySelected}
|
||||
>
|
||||
</WorkflowLibraryViewButton>
|
||||
<WorkflowLibraryViewButton size="sm" rightIcon={<PiUsersBold />} view="shared">
|
||||
{t('workflows.shared')}
|
||||
<Spacer />
|
||||
</CategoryButton>
|
||||
</WorkflowLibraryViewButton>
|
||||
</Flex>
|
||||
</Collapse>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex h="full" minH={0} overflow="hidden" flexDir="column">
|
||||
<CategoryButton isSelected={isDefaultWorkflowsExclusivelySelected} onClick={selectDefaultWorkflows}>
|
||||
{t('workflows.browseWorkflows')}
|
||||
</CategoryButton>
|
||||
<Collapse in={isDefaultWorkflowsExclusivelySelected}>
|
||||
<Flex flexDir="column" gap={2} pl={4} py={2} overflow="hidden" h="100%" minH={0}>
|
||||
<Button
|
||||
isDisabled={!isDefaultWorkflowsExclusivelySelected || tags.length === 0}
|
||||
onClick={resetTags}
|
||||
size="sm"
|
||||
variant="link"
|
||||
fontWeight="bold"
|
||||
justifyContent="flex-start"
|
||||
flexGrow={0}
|
||||
flexShrink={0}
|
||||
leftIcon={<PiArrowCounterClockwiseBold />}
|
||||
h={8}
|
||||
>
|
||||
{t('workflows.deselectAll')}
|
||||
</Button>
|
||||
<Flex flexDir="column" gap={2} overflow="auto">
|
||||
{tagCategoryOptions.map((tagCategory) => (
|
||||
<TagCategory
|
||||
key={tagCategory.categoryTKey}
|
||||
tagCategory={tagCategory}
|
||||
isDisabled={!isDefaultWorkflowsExclusivelySelected}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Collapse>
|
||||
<WorkflowLibraryViewButton view="defaults">{t('workflows.browseWorkflows')}</WorkflowLibraryViewButton>
|
||||
<DefaultsViewCheckboxesCollapsible />
|
||||
</Flex>
|
||||
<Spacer />
|
||||
<NewWorkflowButton />
|
||||
@@ -164,6 +60,45 @@ export const WorkflowLibrarySideNav = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultsViewCheckboxesCollapsible = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const tags = useAppSelector(selectWorkflowLibrarySelectedTags);
|
||||
const tagCategoryOptions = useStore($workflowLibraryTagCategoriesOptions);
|
||||
const view = useAppSelector(selectWorkflowLibraryView);
|
||||
|
||||
const resetTags = useCallback(() => {
|
||||
dispatch(workflowLibraryTagsReset());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<Collapse in={view === 'defaults'}>
|
||||
<Flex flexDir="column" gap={2} pl={4} py={2} overflow="hidden" h="100%" minH={0}>
|
||||
<Button
|
||||
isDisabled={tags.length === 0}
|
||||
onClick={resetTags}
|
||||
size="sm"
|
||||
variant="link"
|
||||
fontWeight="bold"
|
||||
justifyContent="flex-start"
|
||||
flexGrow={0}
|
||||
flexShrink={0}
|
||||
leftIcon={<PiArrowCounterClockwiseBold />}
|
||||
h={8}
|
||||
>
|
||||
{t('workflows.deselectAll')}
|
||||
</Button>
|
||||
<Flex flexDir="column" gap={2} overflow="auto">
|
||||
{tagCategoryOptions.map((tagCategory) => (
|
||||
<TagCategory key={tagCategory.categoryTKey} tagCategory={tagCategory} />
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Collapse>
|
||||
);
|
||||
});
|
||||
DefaultsViewCheckboxesCollapsible.displayName = 'DefaultsViewCheckboxes';
|
||||
|
||||
const useCountForIndividualTag = (tag: string) => {
|
||||
const allTags = useStore($workflowLibraryTagOptions);
|
||||
const queryArg = useMemo(
|
||||
@@ -244,7 +179,13 @@ const RecentWorkflowButton = memo(({ workflow }: { workflow: S['WorkflowRecordLi
|
||||
});
|
||||
RecentWorkflowButton.displayName = 'RecentWorkflowButton';
|
||||
|
||||
const CategoryButton = memo(({ isSelected, ...rest }: ButtonProps & { isSelected: boolean }) => {
|
||||
const WorkflowLibraryViewButton = memo(({ view, ...rest }: ButtonProps & { view: WorkflowLibraryView }) => {
|
||||
const dispatch = useDispatch();
|
||||
const selectedView = useAppSelector(selectWorkflowLibraryView);
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(workflowLibraryViewChanged(view));
|
||||
}, [dispatch, view]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -252,15 +193,16 @@ const CategoryButton = memo(({ isSelected, ...rest }: ButtonProps & { isSelected
|
||||
size="md"
|
||||
flexShrink={0}
|
||||
w="full"
|
||||
onClick={onClick}
|
||||
{...rest}
|
||||
bg={isSelected ? 'base.700' : undefined}
|
||||
color={isSelected ? 'base.50' : undefined}
|
||||
bg={selectedView === view ? 'base.700' : undefined}
|
||||
color={selectedView === view ? 'base.50' : undefined}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CategoryButton.displayName = 'NavButton';
|
||||
WorkflowLibraryViewButton.displayName = 'NavButton';
|
||||
|
||||
const TagCategory = memo(({ tagCategory, isDisabled }: { tagCategory: WorkflowTagCategory; isDisabled: boolean }) => {
|
||||
const TagCategory = memo(({ tagCategory }: { tagCategory: WorkflowTagCategory }) => {
|
||||
const { t } = useTranslation();
|
||||
const count = useCountForTagCategory(tagCategory);
|
||||
|
||||
@@ -270,12 +212,12 @@ const TagCategory = memo(({ tagCategory, isDisabled }: { tagCategory: WorkflowTa
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={2}>
|
||||
<Text fontWeight="semibold" color="base.300" opacity={isDisabled ? 0.5 : 1} flexShrink={0}>
|
||||
<Text fontWeight="semibold" color="base.300" flexShrink={0}>
|
||||
{t(tagCategory.categoryTKey)}
|
||||
</Text>
|
||||
<Flex flexDir="column" gap={2} pl={4}>
|
||||
{tagCategory.tags.map((tag) => (
|
||||
<TagCheckbox key={tag} tag={tag} isDisabled={isDisabled} />
|
||||
<TagCheckbox key={tag} tag={tag} />
|
||||
))}
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -285,7 +227,7 @@ TagCategory.displayName = 'TagCategory';
|
||||
|
||||
const TagCheckbox = memo(({ tag, ...rest }: CheckboxProps & { tag: string }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedTags = useAppSelector(selectWorkflowLibraryTags);
|
||||
const selectedTags = useAppSelector(selectWorkflowLibrarySelectedTags);
|
||||
const isChecked = selectedTags.includes(tag);
|
||||
const count = useCountForIndividualTag(tag);
|
||||
|
||||
|
||||
@@ -2,33 +2,59 @@ import { Button, Flex, Grid, GridItem, Spacer, Spinner } from '@invoke-ai/ui-lib
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import type { WorkflowLibraryView } from 'features/nodes/store/workflowLibrarySlice';
|
||||
import {
|
||||
selectWorkflowLibraryCategories,
|
||||
selectWorkflowLibraryDirection,
|
||||
selectWorkflowLibraryHasSearchTerm,
|
||||
selectWorkflowLibraryOrderBy,
|
||||
selectWorkflowLibrarySearchTerm,
|
||||
selectWorkflowLibraryShowOpenedWorkflowsOnly,
|
||||
selectWorkflowLibraryTags,
|
||||
selectWorkflowLibrarySelectedTags,
|
||||
selectWorkflowLibraryView,
|
||||
} from 'features/nodes/store/workflowLibrarySlice';
|
||||
import type { WorkflowCategory } from 'features/nodes/types/workflow';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useListWorkflowsInfiniteInfiniteQuery } from 'services/api/endpoints/workflows';
|
||||
import type { S } from 'services/api/types';
|
||||
import type { Equals } from 'tsafe';
|
||||
import { assert } from 'tsafe';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { WorkflowListItem } from './WorkflowListItem';
|
||||
|
||||
const PER_PAGE = 30;
|
||||
|
||||
const getCategories = (view: WorkflowLibraryView): WorkflowCategory[] => {
|
||||
switch (view) {
|
||||
case 'defaults':
|
||||
return ['default'];
|
||||
case 'recent':
|
||||
return ['user', 'project', 'default'];
|
||||
case 'yours':
|
||||
return ['user', 'project'];
|
||||
case 'private':
|
||||
return ['user'];
|
||||
case 'shared':
|
||||
return ['project'];
|
||||
default:
|
||||
assert<Equals<typeof view, never>>(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getHasBeenOpened = (view: WorkflowLibraryView): boolean | undefined => {
|
||||
if (view === 'recent') {
|
||||
return true;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const useInfiniteQueryAry = () => {
|
||||
const categories = useAppSelector(selectWorkflowLibraryCategories);
|
||||
const orderBy = useAppSelector(selectWorkflowLibraryOrderBy);
|
||||
const direction = useAppSelector(selectWorkflowLibraryDirection);
|
||||
const query = useAppSelector(selectWorkflowLibrarySearchTerm);
|
||||
const tags = useAppSelector(selectWorkflowLibraryTags);
|
||||
const showOpenedWorkflowsOnly = useAppSelector(selectWorkflowLibraryShowOpenedWorkflowsOnly);
|
||||
const [debouncedQuery] = useDebounce(query, 500);
|
||||
const searchTerm = useAppSelector(selectWorkflowLibrarySearchTerm);
|
||||
const selectedTags = useAppSelector(selectWorkflowLibrarySelectedTags);
|
||||
const view = useAppSelector(selectWorkflowLibraryView);
|
||||
const [debouncedSearchTerm] = useDebounce(searchTerm, 500);
|
||||
|
||||
const queryArg = useMemo(() => {
|
||||
return {
|
||||
@@ -36,12 +62,12 @@ const useInfiniteQueryAry = () => {
|
||||
per_page: PER_PAGE,
|
||||
order_by: orderBy ?? 'opened_at',
|
||||
direction,
|
||||
categories,
|
||||
query: debouncedQuery,
|
||||
tags: categories.length === 1 && categories.includes('default') ? tags : [],
|
||||
has_been_opened: showOpenedWorkflowsOnly || undefined,
|
||||
categories: getCategories(view),
|
||||
query: debouncedSearchTerm,
|
||||
tags: view === 'defaults' ? selectedTags : [],
|
||||
has_been_opened: getHasBeenOpened(view),
|
||||
} satisfies Parameters<typeof useListWorkflowsInfiniteInfiniteQuery>[0];
|
||||
}, [orderBy, direction, categories, debouncedQuery, tags, showOpenedWorkflowsOnly]);
|
||||
}, [orderBy, direction, view, debouncedSearchTerm, selectedTags]);
|
||||
|
||||
return queryArg;
|
||||
};
|
||||
|
||||
@@ -5,22 +5,22 @@ import type { WorkflowCategory } from 'features/nodes/types/workflow';
|
||||
import { atom, computed } from 'nanostores';
|
||||
import type { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
|
||||
|
||||
export type WorkflowLibraryView = 'recent' | 'yours' | 'private' | 'shared' | 'defaults';
|
||||
|
||||
type WorkflowLibraryState = {
|
||||
searchTerm: string;
|
||||
view: WorkflowLibraryView;
|
||||
orderBy: WorkflowRecordOrderBy;
|
||||
direction: SQLiteDirection;
|
||||
tags: string[];
|
||||
categories: WorkflowCategory[];
|
||||
showOpenedWorkflowsOnly: boolean;
|
||||
searchTerm: string;
|
||||
selectedTags: string[];
|
||||
};
|
||||
|
||||
const initialWorkflowLibraryState: WorkflowLibraryState = {
|
||||
searchTerm: '',
|
||||
orderBy: 'opened_at',
|
||||
direction: 'DESC',
|
||||
tags: [],
|
||||
categories: ['user'],
|
||||
showOpenedWorkflowsOnly: false,
|
||||
selectedTags: [],
|
||||
view: 'defaults',
|
||||
};
|
||||
|
||||
export const workflowLibrarySlice = createSlice({
|
||||
@@ -36,24 +36,21 @@ export const workflowLibrarySlice = createSlice({
|
||||
workflowLibraryDirectionChanged: (state, action: PayloadAction<SQLiteDirection>) => {
|
||||
state.direction = action.payload;
|
||||
},
|
||||
workflowLibraryCategoriesChanged: (state, action: PayloadAction<WorkflowCategory[]>) => {
|
||||
state.categories = action.payload;
|
||||
workflowLibraryViewChanged: (state, action: PayloadAction<WorkflowLibraryState['view']>) => {
|
||||
state.view = action.payload;
|
||||
state.searchTerm = '';
|
||||
},
|
||||
workflowLibraryShowOpenedWorkflowsOnlyChanged: (state, action: PayloadAction<boolean>) => {
|
||||
state.showOpenedWorkflowsOnly = action.payload;
|
||||
},
|
||||
workflowLibraryTagToggled: (state, action: PayloadAction<string>) => {
|
||||
const tag = action.payload;
|
||||
const tags = state.tags;
|
||||
const tags = state.selectedTags;
|
||||
if (tags.includes(tag)) {
|
||||
state.tags = tags.filter((t) => t !== tag);
|
||||
state.selectedTags = tags.filter((t) => t !== tag);
|
||||
} else {
|
||||
state.tags = [...tags, tag];
|
||||
state.selectedTags = [...tags, tag];
|
||||
}
|
||||
},
|
||||
workflowLibraryTagsReset: (state) => {
|
||||
state.tags = [];
|
||||
state.selectedTags = [];
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -62,10 +59,9 @@ export const {
|
||||
workflowLibrarySearchTermChanged,
|
||||
workflowLibraryOrderByChanged,
|
||||
workflowLibraryDirectionChanged,
|
||||
workflowLibraryCategoriesChanged,
|
||||
workflowLibraryShowOpenedWorkflowsOnlyChanged,
|
||||
workflowLibraryTagToggled,
|
||||
workflowLibraryTagsReset,
|
||||
workflowLibraryViewChanged,
|
||||
} = workflowLibrarySlice.actions;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
@@ -86,11 +82,10 @@ export const selectWorkflowLibrarySearchTerm = createWorkflowLibrarySelector(({
|
||||
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 selectWorkflowLibraryShowOpenedWorkflowsOnly = createWorkflowLibrarySelector(({ showOpenedWorkflowsOnly }) => showOpenedWorkflowsOnly);
|
||||
export const selectWorkflowLibrarySelectedTags = createWorkflowLibrarySelector(({ selectedTags }) => selectedTags);
|
||||
export const selectWorkflowLibraryView = createWorkflowLibrarySelector(({ view }) => view);
|
||||
|
||||
export const DEFAULT_WORKFLOW_LIBRARY_CATEGORIES = ['user', 'default'] satisfies WorkflowCategory[];
|
||||
export const DEFAULT_WORKFLOW_LIBRARY_CATEGORIES = ['user', 'default', 'project'] satisfies WorkflowCategory[];
|
||||
export const $workflowLibraryCategoriesOptions = atom<WorkflowCategory[]>(DEFAULT_WORKFLOW_LIBRARY_CATEGORIES);
|
||||
|
||||
export type WorkflowTagCategory = { categoryTKey: string; tags: string[] };
|
||||
|
||||
@@ -30,7 +30,8 @@ export const workflowsApi = api.injectEndpoints({
|
||||
// Because this may change the order of the list, we need to invalidate the whole list
|
||||
{ type: 'Workflow', id: LIST_TAG },
|
||||
{ type: 'Workflow', id: workflow_id },
|
||||
'WorkflowTagCountsWithFilter',
|
||||
'WorkflowTagCounts',
|
||||
'WorkflowCategoryCounts',
|
||||
],
|
||||
}),
|
||||
createWorkflow: build.mutation<
|
||||
@@ -45,7 +46,8 @@ export const workflowsApi = api.injectEndpoints({
|
||||
invalidatesTags: [
|
||||
// Because this may change the order of the list, we need to invalidate the whole list
|
||||
{ type: 'Workflow', id: LIST_TAG },
|
||||
'WorkflowTagCountsWithFilter',
|
||||
'WorkflowTagCounts',
|
||||
'WorkflowCategoryCounts',
|
||||
],
|
||||
}),
|
||||
updateWorkflow: build.mutation<
|
||||
@@ -59,7 +61,8 @@ export const workflowsApi = api.injectEndpoints({
|
||||
}),
|
||||
invalidatesTags: (response, error, workflow) => [
|
||||
{ type: 'Workflow', id: workflow.id },
|
||||
'WorkflowTagCountsWithFilter',
|
||||
'WorkflowTagCounts',
|
||||
'WorkflowCategoryCounts',
|
||||
],
|
||||
}),
|
||||
listWorkflows: build.query<
|
||||
@@ -78,7 +81,16 @@ export const workflowsApi = api.injectEndpoints({
|
||||
query: (params) => ({
|
||||
url: `${buildWorkflowsUrl('counts_by_tag')}?${queryString.stringify(params, { arrayFormat: 'none' })}`,
|
||||
}),
|
||||
providesTags: ['WorkflowTagCountsWithFilter'],
|
||||
providesTags: ['WorkflowTagCounts'],
|
||||
}),
|
||||
getCountsByCategory: build.query<
|
||||
paths['/api/v1/workflows/counts_by_category']['get']['responses']['200']['content']['application/json'],
|
||||
NonNullable<paths['/api/v1/workflows/counts_by_category']['get']['parameters']['query']>
|
||||
>({
|
||||
query: (params) => ({
|
||||
url: `${buildWorkflowsUrl('counts_by_category')}?${queryString.stringify(params, { arrayFormat: 'none' })}`,
|
||||
}),
|
||||
providesTags: ['WorkflowCategoryCounts'],
|
||||
}),
|
||||
listWorkflowsInfinite: build.infiniteQuery<
|
||||
paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'],
|
||||
@@ -151,6 +163,7 @@ export const workflowsApi = api.injectEndpoints({
|
||||
export const {
|
||||
useUpdateOpenedAtMutation,
|
||||
useGetCountsByTagQuery,
|
||||
useGetCountsByCategoryQuery,
|
||||
useLazyGetWorkflowQuery,
|
||||
useGetWorkflowQuery,
|
||||
useCreateWorkflowMutation,
|
||||
|
||||
@@ -44,7 +44,8 @@ const tagTypes = [
|
||||
'LoRAModel',
|
||||
'SDXLRefinerModel',
|
||||
'Workflow',
|
||||
'WorkflowTagCountsWithFilter',
|
||||
'WorkflowTagCounts',
|
||||
'WorkflowCategoryCounts',
|
||||
'StylePreset',
|
||||
'Schema',
|
||||
'QueueCountsByDestination',
|
||||
|
||||
Reference in New Issue
Block a user