mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): rough out recent workflows
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { ButtonProps, CheckboxProps } from '@invoke-ai/ui-library';
|
||||
import { Button, Checkbox, Collapse, Flex, Spacer, Text } 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';
|
||||
@@ -11,12 +11,14 @@ import {
|
||||
workflowSelectedTagsRese,
|
||||
workflowSelectedTagToggled,
|
||||
} from 'features/nodes/store/workflowSlice';
|
||||
import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
|
||||
import { UploadWorkflowButton } from 'features/workflowLibrary/components/UploadWorkflowButton';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiUsersBold } from 'react-icons/pi';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useGetCountsQuery } from 'services/api/endpoints/workflows';
|
||||
import { useGetCountsQuery, useListWorkflowsQuery } from 'services/api/endpoints/workflows';
|
||||
import type { S } from 'services/api/types';
|
||||
|
||||
export const WorkflowLibrarySideNav = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -66,8 +68,16 @@ export const WorkflowLibrarySideNav = () => {
|
||||
}, [categories]);
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" h="full">
|
||||
<Flex w="full" pb={2}>
|
||||
<Flex h="full" minH={0} overflow="hidden" flexDir="column" w={64} gap={1}>
|
||||
<Flex flexDir="column" w="full" pb={2}>
|
||||
<Text px={3} py={2} fontSize="md" fontWeight="semibold">
|
||||
{t('workflows.recentlyOpened')}
|
||||
</Text>
|
||||
<Flex flexDir="column" gap={2} pl={4}>
|
||||
<RecentWorkflows />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex flexDir="column" w="full" pb={2}>
|
||||
<CategoryButton isSelected={isYourWorkflowsSelected} onClick={selectYourWorkflows}>
|
||||
{t('workflows.yourWorkflows')}
|
||||
</CategoryButton>
|
||||
@@ -98,7 +108,7 @@ export const WorkflowLibrarySideNav = () => {
|
||||
</Collapse>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex w="full" h="full" minH={0} overflow="hidden" flexDir="column">
|
||||
<Flex h="full" minH={0} overflow="hidden" flexDir="column">
|
||||
<CategoryButton isSelected={isDefaultWorkflowsExclusivelySelected} onClick={selectDefaultWorkflows}>
|
||||
{t('workflows.browseWorkflows')}
|
||||
</CategoryButton>
|
||||
@@ -136,6 +146,60 @@ export const WorkflowLibrarySideNav = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const recentWorkflowsQueryArg = {
|
||||
page: 0,
|
||||
per_page: 5,
|
||||
order_by: 'opened_at',
|
||||
direction: 'DESC',
|
||||
} satisfies Parameters<typeof useListWorkflowsQuery>[0];
|
||||
|
||||
const RecentWorkflows = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading } = useListWorkflowsQuery(recentWorkflowsQueryArg);
|
||||
|
||||
if (isLoading) {
|
||||
return <Text variant="subtext">{t('common.loading')}</Text>;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Text variant="subtext">{t('workflows.noRecentWorkflows')}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.items.map((workflow) => {
|
||||
return <RecentWorkflowButton key={workflow.workflow_id} workflow={workflow} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
RecentWorkflows.displayName = 'RecentWorkflows';
|
||||
|
||||
const RecentWorkflowButton = memo(({ workflow }: { workflow: S['WorkflowRecordListItemWithThumbnailDTO'] }) => {
|
||||
const loadWorkflow = useLoadWorkflow();
|
||||
const load = useCallback(() => {
|
||||
loadWorkflow.loadWithDialog(workflow.workflow_id, 'view');
|
||||
}, [loadWorkflow, workflow.workflow_id]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
role="button"
|
||||
key={workflow.workflow_id}
|
||||
gap={2}
|
||||
alignItems="center"
|
||||
_hover={{ textDecoration: 'underline' }}
|
||||
color="base.300"
|
||||
onClick={load}
|
||||
>
|
||||
<Text as="span" noOfLines={1} w="full" fontWeight="semibold">
|
||||
{workflow.name}
|
||||
</Text>
|
||||
{workflow.category === 'project' && <Icon as={PiUsersBold} boxSize="12px" />}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
RecentWorkflowButton.displayName = 'RecentWorkflowButton';
|
||||
|
||||
const CategoryButton = memo(({ isSelected, ...rest }: ButtonProps & { isSelected: boolean }) => {
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -32,7 +32,7 @@ const useInfiniteQueryAry = () => {
|
||||
return {
|
||||
page: 0,
|
||||
per_page: PER_PAGE,
|
||||
order_by: orderBy,
|
||||
order_by: orderBy ?? 'opened_at',
|
||||
direction,
|
||||
categories,
|
||||
query: debouncedQuery,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Flex, FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $projectId } from 'app/store/nanostores/projectId';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
selectWorkflowOrderBy,
|
||||
@@ -22,7 +20,6 @@ type Direction = z.infer<typeof zDirection>;
|
||||
const isDirection = (v: unknown): v is Direction => zDirection.safeParse(v).success;
|
||||
|
||||
export const WorkflowSortControl = () => {
|
||||
const projectId = useStore($projectId);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const orderBy = useAppSelector(selectWorkflowOrderBy);
|
||||
@@ -68,15 +65,12 @@ export const WorkflowSortControl = () => {
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
// In OSS, we don't have the concept of "opened_at" for workflows. This is only available in the Enterprise version.
|
||||
const defaultOrderBy = projectId !== undefined ? 'opened_at' : 'created_at';
|
||||
|
||||
return (
|
||||
<Flex flexDir="row" gap={6}>
|
||||
<FormControl orientation="horizontal" gap={0} w="auto">
|
||||
<FormLabel>{t('common.orderBy')}</FormLabel>
|
||||
<Select value={orderBy ?? defaultOrderBy} onChange={onChangeOrderBy} size="sm">
|
||||
{projectId !== undefined && <option value="opened_at">{ORDER_BY_LABELS['opened_at']}</option>}
|
||||
<Select value={orderBy ?? 'opened_at'} onChange={onChangeOrderBy} size="sm">
|
||||
<option value="opened_at">{ORDER_BY_LABELS['opened_at']}</option>
|
||||
<option value="created_at">{ORDER_BY_LABELS['created_at']}</option>
|
||||
<option value="updated_at">{ORDER_BY_LABELS['updated_at']}</option>
|
||||
<option value="name">{ORDER_BY_LABELS['name']}</option>
|
||||
|
||||
@@ -84,7 +84,7 @@ const initialWorkflowState: WorkflowState = {
|
||||
mode: 'view',
|
||||
formFieldInitialValues: {},
|
||||
searchTerm: '',
|
||||
orderBy: undefined, // initial value is decided in component
|
||||
orderBy: 'opened_at', // initial value is decided in component
|
||||
orderDirection: 'DESC',
|
||||
selectedTags: [],
|
||||
selectedCategories: ['user'],
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useToast } from '@invoke-ai/ui-library';
|
||||
import { useLoadWorkflow } from 'features/workflowLibrary/hooks/useLoadWorkflow';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLazyGetWorkflowQuery, workflowsApi } from 'services/api/endpoints/workflows';
|
||||
import { useLazyGetWorkflowQuery, useUpdateOpenedAtMutation, workflowsApi } from 'services/api/endpoints/workflows';
|
||||
|
||||
type UseGetAndLoadLibraryWorkflowOptions = {
|
||||
onSuccess?: () => void;
|
||||
@@ -20,13 +20,15 @@ export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = (arg)
|
||||
const toast = useToast();
|
||||
const { t } = useTranslation();
|
||||
const loadWorkflow = useLoadWorkflow();
|
||||
const [_getAndLoadWorkflow, getAndLoadWorkflowResult] = useLazyGetWorkflowQuery();
|
||||
const [getWorkflow, getAndLoadWorkflowResult] = useLazyGetWorkflowQuery();
|
||||
const [updateOpenedAt] = useUpdateOpenedAtMutation();
|
||||
const getAndLoadWorkflow = useCallback(
|
||||
async (workflow_id: string) => {
|
||||
try {
|
||||
const { workflow } = await _getAndLoadWorkflow(workflow_id).unwrap();
|
||||
const { workflow } = await getWorkflow(workflow_id).unwrap();
|
||||
// This action expects a stringified workflow, instead of updating the routes and services we will just stringify it here
|
||||
loadWorkflow({ workflow: JSON.stringify(workflow), graph: null });
|
||||
await loadWorkflow({ workflow: JSON.stringify(workflow), graph: null });
|
||||
updateOpenedAt({ workflow_id });
|
||||
// No toast - the listener for this action does that after the workflow is loaded
|
||||
arg?.onSuccess && arg.onSuccess();
|
||||
} catch {
|
||||
@@ -38,7 +40,7 @@ export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = (arg)
|
||||
arg?.onError && arg.onError();
|
||||
}
|
||||
},
|
||||
[_getAndLoadWorkflow, loadWorkflow, arg, toast, t]
|
||||
[getWorkflow, loadWorkflow, updateOpenedAt, arg, toast, t]
|
||||
);
|
||||
|
||||
return { getAndLoadWorkflow, getAndLoadWorkflowResult };
|
||||
|
||||
Reference in New Issue
Block a user