feat(ui): rough out recent workflows

This commit is contained in:
psychedelicious
2025-03-06 15:38:45 +10:00
parent f657c95e45
commit 50657650c2
8 changed files with 90 additions and 21 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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'],

View File

@@ -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 };