Skip to content

Commit 5065765

Browse files
feat(ui): rough out recent workflows
1 parent f657c95 commit 5065765

File tree

8 files changed

+90
-21
lines changed

8 files changed

+90
-21
lines changed

invokeai/frontend/web/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1691,6 +1691,7 @@
16911691
"searchPlaceholder": "Search by name, description or tags",
16921692
"filterByTags": "Filter by Tags",
16931693
"yourWorkflows": "Your Workflows",
1694+
"recentlyOpened": "Recently Opened",
16941695
"private": "Private",
16951696
"shared": "Shared",
16961697
"browseWorkflows": "Browse Workflows",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
import type { WorkflowCategory } from 'features/nodes/types/workflow';
22
import { atom } from 'nanostores';
33

4-
export const $workflowCategories = atom<WorkflowCategory[]>(['user', 'default']);
4+
export const $workflowCategories = atom<WorkflowCategory[]>(['user', 'default', 'project']);

invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ButtonProps, CheckboxProps } from '@invoke-ai/ui-library';
2-
import { Button, Checkbox, Collapse, Flex, Spacer, Text } from '@invoke-ai/ui-library';
2+
import { Button, Checkbox, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-library';
33
import { useStore } from '@nanostores/react';
44
import { $workflowCategories } from 'app/store/nanostores/workflowCategories';
55
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
@@ -11,12 +11,14 @@ import {
1111
workflowSelectedTagsRese,
1212
workflowSelectedTagToggled,
1313
} from 'features/nodes/store/workflowSlice';
14+
import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog';
1415
import { UploadWorkflowButton } from 'features/workflowLibrary/components/UploadWorkflowButton';
1516
import { memo, useCallback, useMemo } from 'react';
1617
import { useTranslation } from 'react-i18next';
1718
import { PiArrowCounterClockwiseBold, PiUsersBold } from 'react-icons/pi';
1819
import { useDispatch } from 'react-redux';
19-
import { useGetCountsQuery } from 'services/api/endpoints/workflows';
20+
import { useGetCountsQuery, useListWorkflowsQuery } from 'services/api/endpoints/workflows';
21+
import type { S } from 'services/api/types';
2022

2123
export const WorkflowLibrarySideNav = () => {
2224
const { t } = useTranslation();
@@ -66,8 +68,16 @@ export const WorkflowLibrarySideNav = () => {
6668
}, [categories]);
6769

6870
return (
69-
<Flex flexDir="column" h="full">
70-
<Flex w="full" pb={2}>
71+
<Flex h="full" minH={0} overflow="hidden" flexDir="column" w={64} gap={1}>
72+
<Flex flexDir="column" w="full" pb={2}>
73+
<Text px={3} py={2} fontSize="md" fontWeight="semibold">
74+
{t('workflows.recentlyOpened')}
75+
</Text>
76+
<Flex flexDir="column" gap={2} pl={4}>
77+
<RecentWorkflows />
78+
</Flex>
79+
</Flex>
80+
<Flex flexDir="column" w="full" pb={2}>
7181
<CategoryButton isSelected={isYourWorkflowsSelected} onClick={selectYourWorkflows}>
7282
{t('workflows.yourWorkflows')}
7383
</CategoryButton>
@@ -98,7 +108,7 @@ export const WorkflowLibrarySideNav = () => {
98108
</Collapse>
99109
)}
100110
</Flex>
101-
<Flex w="full" h="full" minH={0} overflow="hidden" flexDir="column">
111+
<Flex h="full" minH={0} overflow="hidden" flexDir="column">
102112
<CategoryButton isSelected={isDefaultWorkflowsExclusivelySelected} onClick={selectDefaultWorkflows}>
103113
{t('workflows.browseWorkflows')}
104114
</CategoryButton>
@@ -136,6 +146,60 @@ export const WorkflowLibrarySideNav = () => {
136146
);
137147
};
138148

149+
const recentWorkflowsQueryArg = {
150+
page: 0,
151+
per_page: 5,
152+
order_by: 'opened_at',
153+
direction: 'DESC',
154+
} satisfies Parameters<typeof useListWorkflowsQuery>[0];
155+
156+
const RecentWorkflows = memo(() => {
157+
const { t } = useTranslation();
158+
const { data, isLoading } = useListWorkflowsQuery(recentWorkflowsQueryArg);
159+
160+
if (isLoading) {
161+
return <Text variant="subtext">{t('common.loading')}</Text>;
162+
}
163+
164+
if (!data) {
165+
return <Text variant="subtext">{t('workflows.noRecentWorkflows')}</Text>;
166+
}
167+
168+
return (
169+
<>
170+
{data.items.map((workflow) => {
171+
return <RecentWorkflowButton key={workflow.workflow_id} workflow={workflow} />;
172+
})}
173+
</>
174+
);
175+
});
176+
RecentWorkflows.displayName = 'RecentWorkflows';
177+
178+
const RecentWorkflowButton = memo(({ workflow }: { workflow: S['WorkflowRecordListItemWithThumbnailDTO'] }) => {
179+
const loadWorkflow = useLoadWorkflow();
180+
const load = useCallback(() => {
181+
loadWorkflow.loadWithDialog(workflow.workflow_id, 'view');
182+
}, [loadWorkflow, workflow.workflow_id]);
183+
184+
return (
185+
<Flex
186+
role="button"
187+
key={workflow.workflow_id}
188+
gap={2}
189+
alignItems="center"
190+
_hover={{ textDecoration: 'underline' }}
191+
color="base.300"
192+
onClick={load}
193+
>
194+
<Text as="span" noOfLines={1} w="full" fontWeight="semibold">
195+
{workflow.name}
196+
</Text>
197+
{workflow.category === 'project' && <Icon as={PiUsersBold} boxSize="12px" />}
198+
</Flex>
199+
);
200+
});
201+
RecentWorkflowButton.displayName = 'RecentWorkflowButton';
202+
139203
const CategoryButton = memo(({ isSelected, ...rest }: ButtonProps & { isSelected: boolean }) => {
140204
return (
141205
<Button

invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const useInfiniteQueryAry = () => {
3232
return {
3333
page: 0,
3434
per_page: PER_PAGE,
35-
order_by: orderBy,
35+
order_by: orderBy ?? 'opened_at',
3636
direction,
3737
categories,
3838
query: debouncedQuery,

invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSortControl.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { Flex, FormControl, FormLabel, Select } from '@invoke-ai/ui-library';
2-
import { useStore } from '@nanostores/react';
3-
import { $projectId } from 'app/store/nanostores/projectId';
42
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
53
import {
64
selectWorkflowOrderBy,
@@ -22,7 +20,6 @@ type Direction = z.infer<typeof zDirection>;
2220
const isDirection = (v: unknown): v is Direction => zDirection.safeParse(v).success;
2321

2422
export const WorkflowSortControl = () => {
25-
const projectId = useStore($projectId);
2623
const { t } = useTranslation();
2724

2825
const orderBy = useAppSelector(selectWorkflowOrderBy);
@@ -68,15 +65,12 @@ export const WorkflowSortControl = () => {
6865
[dispatch]
6966
);
7067

71-
// In OSS, we don't have the concept of "opened_at" for workflows. This is only available in the Enterprise version.
72-
const defaultOrderBy = projectId !== undefined ? 'opened_at' : 'created_at';
73-
7468
return (
7569
<Flex flexDir="row" gap={6}>
7670
<FormControl orientation="horizontal" gap={0} w="auto">
7771
<FormLabel>{t('common.orderBy')}</FormLabel>
78-
<Select value={orderBy ?? defaultOrderBy} onChange={onChangeOrderBy} size="sm">
79-
{projectId !== undefined && <option value="opened_at">{ORDER_BY_LABELS['opened_at']}</option>}
72+
<Select value={orderBy ?? 'opened_at'} onChange={onChangeOrderBy} size="sm">
73+
<option value="opened_at">{ORDER_BY_LABELS['opened_at']}</option>
8074
<option value="created_at">{ORDER_BY_LABELS['created_at']}</option>
8175
<option value="updated_at">{ORDER_BY_LABELS['updated_at']}</option>
8276
<option value="name">{ORDER_BY_LABELS['name']}</option>

invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const initialWorkflowState: WorkflowState = {
8484
mode: 'view',
8585
formFieldInitialValues: {},
8686
searchTerm: '',
87-
orderBy: undefined, // initial value is decided in component
87+
orderBy: 'opened_at', // initial value is decided in component
8888
orderDirection: 'DESC',
8989
selectedTags: [],
9090
selectedCategories: ['user'],

invokeai/frontend/web/src/features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useToast } from '@invoke-ai/ui-library';
22
import { useLoadWorkflow } from 'features/workflowLibrary/hooks/useLoadWorkflow';
33
import { useCallback } from 'react';
44
import { useTranslation } from 'react-i18next';
5-
import { useLazyGetWorkflowQuery, workflowsApi } from 'services/api/endpoints/workflows';
5+
import { useLazyGetWorkflowQuery, useUpdateOpenedAtMutation, workflowsApi } from 'services/api/endpoints/workflows';
66

77
type UseGetAndLoadLibraryWorkflowOptions = {
88
onSuccess?: () => void;
@@ -20,13 +20,15 @@ export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = (arg)
2020
const toast = useToast();
2121
const { t } = useTranslation();
2222
const loadWorkflow = useLoadWorkflow();
23-
const [_getAndLoadWorkflow, getAndLoadWorkflowResult] = useLazyGetWorkflowQuery();
23+
const [getWorkflow, getAndLoadWorkflowResult] = useLazyGetWorkflowQuery();
24+
const [updateOpenedAt] = useUpdateOpenedAtMutation();
2425
const getAndLoadWorkflow = useCallback(
2526
async (workflow_id: string) => {
2627
try {
27-
const { workflow } = await _getAndLoadWorkflow(workflow_id).unwrap();
28+
const { workflow } = await getWorkflow(workflow_id).unwrap();
2829
// This action expects a stringified workflow, instead of updating the routes and services we will just stringify it here
29-
loadWorkflow({ workflow: JSON.stringify(workflow), graph: null });
30+
await loadWorkflow({ workflow: JSON.stringify(workflow), graph: null });
31+
updateOpenedAt({ workflow_id });
3032
// No toast - the listener for this action does that after the workflow is loaded
3133
arg?.onSuccess && arg.onSuccess();
3234
} catch {
@@ -38,7 +40,7 @@ export const useGetAndLoadLibraryWorkflow: UseGetAndLoadLibraryWorkflow = (arg)
3840
arg?.onError && arg.onError();
3941
}
4042
},
41-
[_getAndLoadWorkflow, loadWorkflow, arg, toast, t]
43+
[getWorkflow, loadWorkflow, updateOpenedAt, arg, toast, t]
4244
);
4345

4446
return { getAndLoadWorkflow, getAndLoadWorkflowResult };

invokeai/frontend/web/src/services/api/endpoints/workflows.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ export const workflowsApi = api.injectEndpoints({
109109
},
110110
},
111111
}),
112+
updateOpenedAt: build.mutation<void, { workflow_id: string }>({
113+
query: ({ workflow_id }) => ({
114+
url: buildWorkflowsUrl(`i/${workflow_id}/opened_at`),
115+
method: 'PUT',
116+
}),
117+
invalidatesTags: (result, error, { workflow_id }) => [{ type: 'Workflow', id: workflow_id }],
118+
}),
112119
setWorkflowThumbnail: build.mutation<void, { workflow_id: string; image: File }>({
113120
query: ({ workflow_id, image }) => {
114121
const formData = new FormData();
@@ -138,6 +145,7 @@ export const workflowsApi = api.injectEndpoints({
138145
});
139146

140147
export const {
148+
useUpdateOpenedAtMutation,
141149
useGetCountsQuery,
142150
useLazyGetWorkflowQuery,
143151
useGetWorkflowQuery,

0 commit comments

Comments
 (0)