From d5c5e8e8eda2ba1043be8ea18a72eabbf9971bcd Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Thu, 27 Feb 2025 13:31:46 -0500 Subject: [PATCH] another new workflow library --- invokeai/app/api/routers/workflows.py | 4 +- .../workflow_records/workflow_records_base.py | 2 +- .../workflow_records_sqlite.py | 62 +++-- .../frontend/web/src/app/components/App.tsx | 5 +- .../web/src/app/hooks/useStudioInitAction.ts | 4 +- .../WorkflowListMenu/WorkflowList.tsx | 83 ------- .../WorkflowListMenu/WorkflowListItem.tsx | 194 ---------------- .../WorkflowListMenuTrigger.tsx | 85 ++----- .../sidePanel/viewMode/EmptyState.tsx | 6 +- .../WorkflowLibrary}/ShareWorkflowModal.tsx | 0 .../WorkflowLibrary/WorkflowLibraryModal.tsx | 38 +++ .../WorkflowLibraryPagination.tsx | 81 +++++++ .../WorkflowLibrarySideNav.tsx | 89 +++++++ .../WorkflowLibrary/WorkflowLibraryTopNav.tsx | 15 ++ .../workflow/WorkflowLibrary/WorkflowList.tsx | 97 ++++++++ .../WorkflowLibrary/WorkflowListItem.tsx | 219 ++++++++++++++++++ .../WorkflowListItemTooltip.tsx | 0 .../WorkflowLibrary}/WorkflowSearch.tsx | 44 ++-- .../WorkflowLibrary}/WorkflowSortControl.tsx | 64 ++--- .../web/src/features/nodes/store/types.ts | 3 + .../nodes/store/workflowLibraryModal.ts | 6 + .../features/nodes/store/workflowListMenu.ts | 6 - .../src/features/nodes/store/workflowSlice.ts | 14 +- .../LoadWorkflowConfirmationAlertDialog.tsx | 8 +- .../components/UploadWorkflowButton.tsx | 6 +- .../src/services/api/endpoints/workflows.ts | 4 +- .../frontend/web/src/services/api/schema.ts | 4 +- 27 files changed, 682 insertions(+), 461 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowList.tsx delete mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListItem.tsx rename invokeai/frontend/web/src/features/nodes/components/sidePanel/{WorkflowListMenu => workflow/WorkflowLibrary}/ShareWorkflowModal.tsx (100%) create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryPagination.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryTopNav.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowListItem.tsx rename invokeai/frontend/web/src/features/nodes/components/sidePanel/{WorkflowListMenu => workflow/WorkflowLibrary}/WorkflowListItemTooltip.tsx (100%) rename invokeai/frontend/web/src/features/nodes/components/sidePanel/{WorkflowListMenu => workflow/WorkflowLibrary}/WorkflowSearch.tsx (66%) rename invokeai/frontend/web/src/features/nodes/components/sidePanel/{WorkflowListMenu => workflow/WorkflowLibrary}/WorkflowSortControl.tsx (55%) create mode 100644 invokeai/frontend/web/src/features/nodes/store/workflowLibraryModal.ts delete mode 100644 invokeai/frontend/web/src/features/nodes/store/workflowListMenu.ts diff --git a/invokeai/app/api/routers/workflows.py b/invokeai/app/api/routers/workflows.py index 07f7b9ad0c..439f4353ce 100644 --- a/invokeai/app/api/routers/workflows.py +++ b/invokeai/app/api/routers/workflows.py @@ -102,13 +102,13 @@ async def list_workflows( default=WorkflowRecordOrderBy.Name, description="The attribute to order by" ), direction: SQLiteDirection = Query(default=SQLiteDirection.Ascending, description="The direction to order by"), - category: WorkflowCategory = Query(default=WorkflowCategory.User, description="The category of workflow to get"), + categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories of workflow to get"), query: Optional[str] = Query(default=None, description="The text to query by (matches name and description)"), ) -> PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]: """Gets a page of workflows""" workflows_with_thumbnails: list[WorkflowRecordListItemWithThumbnailDTO] = [] workflows = ApiDependencies.invoker.services.workflow_records.get_many( - order_by=order_by, direction=direction, page=page, per_page=per_page, query=query, category=category + order_by=order_by, direction=direction, page=page, per_page=per_page, query=query, categories=categories ) for workflow in workflows.items: workflows_with_thumbnails.append( diff --git a/invokeai/app/services/workflow_records/workflow_records_base.py b/invokeai/app/services/workflow_records/workflow_records_base.py index 9da830eaaf..a884c7b7ed 100644 --- a/invokeai/app/services/workflow_records/workflow_records_base.py +++ b/invokeai/app/services/workflow_records/workflow_records_base.py @@ -41,7 +41,7 @@ class WorkflowRecordsStorageBase(ABC): self, order_by: WorkflowRecordOrderBy, direction: SQLiteDirection, - category: WorkflowCategory, + categories: Optional[list[WorkflowCategory]], page: int, per_page: Optional[int], query: Optional[str], diff --git a/invokeai/app/services/workflow_records/workflow_records_sqlite.py b/invokeai/app/services/workflow_records/workflow_records_sqlite.py index d652530b0a..a54fc916a7 100644 --- a/invokeai/app/services/workflow_records/workflow_records_sqlite.py +++ b/invokeai/app/services/workflow_records/workflow_records_sqlite.py @@ -120,7 +120,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): self, order_by: WorkflowRecordOrderBy, direction: SQLiteDirection, - category: WorkflowCategory, + categories: Optional[list[WorkflowCategory]], page: int = 0, per_page: Optional[int] = None, query: Optional[str] = None, @@ -128,28 +128,50 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): # sanitize! assert order_by in WorkflowRecordOrderBy assert direction in SQLiteDirection - assert category in WorkflowCategory - count_query = "SELECT COUNT(*) FROM workflow_library WHERE category = ?" - main_query = """ - SELECT - workflow_id, - category, - name, - description, - created_at, - updated_at, - opened_at - FROM workflow_library - WHERE category = ? - """ - main_params: list[int | str] = [category.value] - count_params: list[int | str] = [category.value] + if categories: + assert all(c in WorkflowCategory for c in categories) + count_query = "SELECT COUNT(*) FROM workflow_library WHERE category IN ({})".format( + ", ".join("?" for _ in categories) + ) + main_query = """ + SELECT + workflow_id, + category, + name, + description, + created_at, + updated_at, + opened_at + FROM workflow_library + WHERE category IN ({}) + """.format(", ".join("?" for _ in categories)) + main_params: list[int | str] = [category.value for category in categories] + count_params: list[int | str] = [category.value for category in categories] + else: + count_query = "SELECT COUNT(*) FROM workflow_library" + main_query = """ + SELECT + workflow_id, + category, + name, + description, + created_at, + updated_at, + opened_at + FROM workflow_library + """ + main_params: list[int | str] = [] + count_params: list[int | str] = [] stripped_query = query.strip() if query else None if stripped_query: wildcard_query = "%" + stripped_query + "%" - main_query += " AND name LIKE ? OR description LIKE ? " - count_query += " AND name LIKE ? OR description LIKE ?;" + if categories: + main_query += " AND (name LIKE ? OR description LIKE ?) " + count_query += " AND (name LIKE ? OR description LIKE ?);" + else: + main_query += " WHERE name LIKE ? OR description LIKE ? " + count_query += " WHERE name LIKE ? OR description LIKE ?;" main_params.extend([wildcard_query, wildcard_query]) count_params.extend([wildcard_query, wildcard_query]) @@ -232,7 +254,7 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase): library_workflows_from_db = self.get_many( order_by=WorkflowRecordOrderBy.Name, direction=SQLiteDirection.Ascending, - category=WorkflowCategory.Default, + categories=[WorkflowCategory.Default], ).items workflows_from_file_ids = [w.id for w in workflows_from_file] diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index ea9fa81311..78b22e8f8e 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -26,7 +26,8 @@ import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicP import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal'; import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast'; -import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal'; +import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal'; +import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal'; import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/ClearQueueConfirmationAlertDialog'; import { useReadinessWatcher } from 'features/queue/store/readiness'; @@ -50,7 +51,6 @@ import { useGetOpenAPISchemaQuery } from 'services/api/endpoints/appInfo'; import { useSocketIO } from 'services/events/useSocketIO'; import AppErrorBoundaryFallback from './AppErrorBoundaryFallback'; - const DEFAULT_CONFIG = {}; interface Props { @@ -129,6 +129,7 @@ const ModalIsolator = memo(() => { + diff --git a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts index 9b7b38427e..8af256ad38 100644 --- a/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts +++ b/invokeai/frontend/web/src/app/hooks/useStudioInitAction.ts @@ -11,7 +11,7 @@ import { $imageViewer } from 'features/gallery/components/ImageViewer/useImageVi import { sentImageToCanvas } from 'features/gallery/store/actions'; import { parseAndRecallAllMetadata } from 'features/metadata/util/handlers'; import { $hasTemplates } from 'features/nodes/store/nodesSlice'; -import { $isWorkflowListMenuIsOpen } from 'features/nodes/store/workflowListMenu'; +import { $isWorkflowLibraryModalOpen } from 'features/nodes/store/workflowLibraryModal'; import { $isStylePresetsMenuOpen, activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice'; import { toast } from 'features/toast/toast'; import { activeTabCanvasRightPanelChanged, setActiveTab } from 'features/ui/store/uiSlice'; @@ -166,7 +166,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => { case 'viewAllWorkflows': // Go to the workflows tab and open the workflow library modal store.dispatch(setActiveTab('workflows')); - $isWorkflowListMenuIsOpen.set(true); + $isWorkflowLibraryModalOpen.set(true); break; case 'viewAllStylePresets': // Go to the canvas tab and open the style presets menu diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowList.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowList.tsx deleted file mode 100644 index 62eaf3316c..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowList.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Button, Collapse, Flex, Icon, Spinner, Text } from '@invoke-ai/ui-library'; -import { EMPTY_ARRAY } from 'app/store/constants'; -import { useAppSelector } from 'app/store/storeHooks'; -import { IAINoContentFallback } from 'common/components/IAIImageFallback'; -import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles'; -import { useCategorySections } from 'features/nodes/hooks/useCategorySections'; -import { - selectWorkflowOrderBy, - selectWorkflowOrderDirection, - selectWorkflowSearchTerm, -} from 'features/nodes/store/workflowSlice'; -import type { WorkflowCategory } from 'features/nodes/types/workflow'; -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretDownBold } from 'react-icons/pi'; -import { useListWorkflowsQuery } from 'services/api/endpoints/workflows'; - -import { WorkflowListItem } from './WorkflowListItem'; - -export const WorkflowList = ({ category }: { category: WorkflowCategory }) => { - const searchTerm = useAppSelector(selectWorkflowSearchTerm); - const orderBy = useAppSelector(selectWorkflowOrderBy); - const direction = useAppSelector(selectWorkflowOrderDirection); - const { t } = useTranslation(); - - const queryArg = useMemo[0]>(() => { - if (category !== 'default') { - return { - order_by: orderBy, - direction, - category: category, - }; - } - return { - order_by: 'name' as const, - direction: 'ASC' as const, - category: category, - }; - }, [category, direction, orderBy]); - - const { data, isLoading } = useListWorkflowsQuery(queryArg, { - selectFromResult: ({ data, isLoading }) => { - const filteredData = - data?.items.filter((workflow) => workflow.name.toLowerCase().includes(searchTerm.toLowerCase())) || EMPTY_ARRAY; - - return { - data: filteredData, - isLoading, - }; - }, - }); - - const { isOpen, onToggle } = useCategorySections(category); - - return ( - - - - {isLoading ? ( - - - - ) : data.length ? ( - data.map((workflow) => ) - ) : ( - - )} - - - ); -}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListItem.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListItem.tsx deleted file mode 100644 index 68355552b7..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListItem.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { Badge, Flex, IconButton, Spacer, Text, Tooltip } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { $projectUrl } from 'app/store/nanostores/projectId'; -import { useAppSelector } from 'app/store/storeHooks'; -import dateFormat, { masks } from 'dateformat'; -import { selectWorkflowId } from 'features/nodes/store/workflowSlice'; -import { useDeleteWorkflow } from 'features/workflowLibrary/components/DeleteLibraryWorkflowConfirmationAlertDialog'; -import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog'; -import { useDownloadWorkflowById } from 'features/workflowLibrary/hooks/useDownloadWorkflowById'; -import type { MouseEvent } from 'react'; -import { useCallback, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiDownloadSimpleBold, PiPencilBold, PiShareFatBold, PiTrashBold } from 'react-icons/pi'; -import type { WorkflowRecordListItemWithThumbnailDTO } from 'services/api/types'; - -import { useShareWorkflow } from './ShareWorkflowModal'; -import { WorkflowListItemTooltip } from './WorkflowListItemTooltip'; - -export const WorkflowListItem = ({ workflow }: { workflow: WorkflowRecordListItemWithThumbnailDTO }) => { - const { t } = useTranslation(); - const projectUrl = useStore($projectUrl); - const [isHovered, setIsHovered] = useState(false); - - const handleMouseOver = useCallback(() => { - setIsHovered(true); - }, []); - - const handleMouseOut = useCallback(() => { - setIsHovered(false); - }, []); - - const workflowId = useAppSelector(selectWorkflowId); - const { downloadWorkflow, isLoading: isLoadingDownloadWorkflow } = useDownloadWorkflowById(); - const shareWorkflow = useShareWorkflow(); - const deleteWorkflow = useDeleteWorkflow(); - const loadWorkflow = useLoadWorkflow(); - - const isActive = useMemo(() => { - return workflowId === workflow.workflow_id; - }, [workflowId, workflow.workflow_id]); - - const handleClickLoad = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - setIsHovered(false); - loadWorkflow.loadWithDialog(workflow.workflow_id, 'view'); - }, - [loadWorkflow, workflow.workflow_id] - ); - - const handleClickEdit = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - setIsHovered(false); - loadWorkflow.loadWithDialog(workflow.workflow_id, 'edit'); - }, - [loadWorkflow, workflow.workflow_id] - ); - - const handleClickDelete = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - setIsHovered(false); - deleteWorkflow(workflow); - }, - [deleteWorkflow, workflow] - ); - - const handleClickShare = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - setIsHovered(false); - shareWorkflow(workflow); - }, - [shareWorkflow, workflow] - ); - - const handleClickDownload = useCallback( - (e: MouseEvent) => { - e.stopPropagation(); - setIsHovered(false); - downloadWorkflow(workflow.workflow_id); - }, - [downloadWorkflow, workflow.workflow_id] - ); - - return ( - - } closeOnScroll> - - - {workflow.name} - - {isActive && ( - - {t('workflows.opened')} - - )} - - {workflow.category !== 'default' && ( - - {t('common.updated')}: {dateFormat(workflow.updated_at, masks.shortDate)}{' '} - {dateFormat(workflow.updated_at, masks.shortTime)} - - )} - - - - - - - } - /> - - - } - isLoading={isLoadingDownloadWorkflow} - /> - - {!!projectUrl && workflow.workflow_id && workflow.category !== 'user' && ( - - } - /> - - )} - {workflow.category !== 'default' && ( - - } - /> - - )} - - - ); -}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger.tsx index c2fe14669f..2b8fe8bd20 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListMenuTrigger.tsx @@ -1,82 +1,27 @@ -import { - Box, - Button, - Flex, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, - Portal, - Text, -} from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; -import { $workflowCategories } from 'app/store/nanostores/workflowCategories'; +import { Button, Text } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu'; +import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal'; import { selectWorkflowName } from 'features/nodes/store/workflowSlice'; -import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton'; -import UploadWorkflowButton from 'features/workflowLibrary/components/UploadWorkflowButton'; -import { useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { PiFolderOpenFill } from 'react-icons/pi'; -import { WorkflowList } from './WorkflowList'; -import { WorkflowSearch } from './WorkflowSearch'; -import { WorkflowSortControl } from './WorkflowSortControl'; - export const WorkflowListMenuTrigger = () => { - const workflowListMenu = useWorkflowListMenu(); + const workflowLibraryModal = useWorkflowLibraryModal(); const { t } = useTranslation(); - const workflowCategories = useStore($workflowCategories); - const searchInputRef = useRef(null); const workflowName = useAppSelector(selectWorkflowName); return ( - - - - - - - - - - - - - - - - - {workflowCategories.map((category) => ( - - ))} - - - - - - - + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/EmptyState.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/EmptyState.tsx index b23803ef22..861ca8adce 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/EmptyState.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/viewMode/EmptyState.tsx @@ -1,6 +1,6 @@ import { Button, Flex, Image, Link, Text } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu'; +import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal'; import { selectCleanEditor, workflowModeChanged } from 'features/nodes/store/workflowSlice'; import InvokeLogoSVG from 'public/assets/images/invoke-symbol-wht-lrg.svg'; import { useCallback } from 'react'; @@ -40,7 +40,7 @@ export const EmptyState = () => { const CleanEditorContent = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const workflowListMenu = useWorkflowListMenu(); + const workflowLibraryModal = useWorkflowLibraryModal(); const onClickNewWorkflow = useCallback(() => { dispatch(workflowModeChanged('edit')); @@ -52,7 +52,7 @@ const CleanEditorContent = () => { - diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ShareWorkflowModal.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal.tsx new file mode 100644 index 0000000000..940dec4570 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal.tsx @@ -0,0 +1,38 @@ +import { + Flex, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, +} from '@invoke-ai/ui-library'; +import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal'; + +import { WorkflowLibrarySideNav } from './WorkflowLibrarySideNav'; +import { WorkflowLibraryTopNav } from './WorkflowLibraryTopNav'; +import { WorkflowList } from './WorkflowList'; + +export const WorkflowLibraryModal = () => { + const workflowLibraryModal = useWorkflowLibraryModal(); + return ( + + + + Workflow Library + + + + + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryPagination.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryPagination.tsx new file mode 100644 index 0000000000..7a076a15db --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryPagination.tsx @@ -0,0 +1,81 @@ +import { Button, Flex, IconButton } from '@invoke-ai/ui-library'; +import type { Dispatch, SetStateAction } from 'react'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi'; +import type { paths } from 'services/api/schema'; + +const PAGES_TO_DISPLAY = 5; + +type PageData = { + page: number; + onClick: () => void; +}; + +type Props = { + page: number; + setPage: Dispatch>; + data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json']; +}; + +export const WorkflowLibraryPagination = ({ page, setPage, data }: Props) => { + const { t } = useTranslation(); + + const handlePrevPage = useCallback(() => { + setPage((p) => Math.max(p - 1, 0)); + }, [setPage]); + + const handleNextPage = useCallback(() => { + setPage((p) => Math.min(p + 1, data.pages - 1)); + }, [data.pages, setPage]); + + const pages: PageData[] = useMemo(() => { + const pages = []; + let first = data.pages > PAGES_TO_DISPLAY ? Math.max(0, page - Math.floor(PAGES_TO_DISPLAY / 2)) : 0; + const last = data.pages > PAGES_TO_DISPLAY ? Math.min(data.pages, first + PAGES_TO_DISPLAY) : data.pages; + if (last - first < PAGES_TO_DISPLAY && data.pages > PAGES_TO_DISPLAY) { + first = last - PAGES_TO_DISPLAY; + } + for (let i = first; i < last; i++) { + pages.push({ + page: i, + onClick: () => setPage(i), + }); + } + return pages; + }, [data.pages, page, setPage]); + + return ( + + } + /> + + {pages.map((p) => ( + + ))} + } + /> + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx new file mode 100644 index 0000000000..753551490b --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibrarySideNav.tsx @@ -0,0 +1,89 @@ +/* eslint-disable i18next/no-literal-string */ +import { Button, Flex } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $workflowCategories } from 'app/store/nanostores/workflowCategories'; +import { useAppSelector } from 'app/store/storeHooks'; +import type { WorkflowLibraryCategory } from 'features/nodes/store/types'; +import { selectWorkflowBrowsingCategory, workflowBrowsingCategoryChanged } from 'features/nodes/store/workflowSlice'; +import { useCallback } from 'react'; +import { PiUsersBold } from 'react-icons/pi'; +import { useDispatch } from 'react-redux'; + +export const WorkflowLibrarySideNav = () => { + const dispatch = useDispatch(); + const browsingCategory = useAppSelector(selectWorkflowBrowsingCategory); + const workflowCategories = useStore($workflowCategories); + + const handleCategoryChange = useCallback( + (category: WorkflowLibraryCategory) => { + dispatch(workflowBrowsingCategoryChanged(category)); + }, + [dispatch] + ); + + return ( + + + {workflowCategories.includes('project') && ( + + + + + )} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryTopNav.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryTopNav.tsx new file mode 100644 index 0000000000..01c7545e23 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryTopNav.tsx @@ -0,0 +1,15 @@ +import { Flex } from '@invoke-ai/ui-library'; +import { useRef } from 'react'; + +import { WorkflowSearch } from './WorkflowSearch'; +import { WorkflowSortControl } from './WorkflowSortControl'; + +export const WorkflowLibraryTopNav = () => { + const searchInputRef = useRef(null); + return ( + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx new file mode 100644 index 0000000000..2655fc35f1 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowList.tsx @@ -0,0 +1,97 @@ +import { Flex, Grid, GridItem, Spinner } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import type { WorkflowLibraryCategory } from 'features/nodes/store/types'; +import { + selectWorkflowBrowsingCategory, + selectWorkflowOrderBy, + selectWorkflowOrderDirection, + selectWorkflowSearchTerm, +} from 'features/nodes/store/workflowSlice'; +import type { WorkflowCategory } from 'features/nodes/types/workflow'; +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useListWorkflowsQuery } from 'services/api/endpoints/workflows'; +import { useDebounce } from 'use-debounce'; + +import { WorkflowLibraryPagination } from './WorkflowLibraryPagination'; +import { WorkflowListItem } from './WorkflowListItem'; + +const PER_PAGE = 6; + +const mapUiCategoryToApiCategory = (sideNav: WorkflowLibraryCategory): WorkflowCategory[] => { + switch (sideNav) { + case 'account': + return ['user', 'project']; + case 'private': + return ['user']; + case 'shared': + return ['project']; + case 'default': + return ['default']; + default: + return []; + } +}; + +export const WorkflowList = () => { + const searchTerm = useAppSelector(selectWorkflowSearchTerm); + const { t } = useTranslation(); + + const [page, setPage] = useState(0); + const browsingCategory = useAppSelector(selectWorkflowBrowsingCategory); + const orderBy = useAppSelector(selectWorkflowOrderBy); + const direction = useAppSelector(selectWorkflowOrderDirection); + const query = useAppSelector(selectWorkflowSearchTerm); + const [debouncedQuery] = useDebounce(query, 500); + + useEffect(() => { + setPage(0); + }, [browsingCategory, query]); + + const queryArg = useMemo[0]>(() => { + const categories = mapUiCategoryToApiCategory(browsingCategory); + return { + page, + per_page: PER_PAGE, + order_by: orderBy, + direction, + categories, + query: debouncedQuery, + }; + }, [direction, orderBy, page, browsingCategory, debouncedQuery]); + + const { data, isLoading } = useListWorkflowsQuery(queryArg); + + if (isLoading) { + return ( + + + + ); + } + + if (!data?.items.length) { + return ( + + ); + } + + return ( + + + {data?.items.map((workflow) => ( + + + + ))} + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowListItem.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowListItem.tsx new file mode 100644 index 0000000000..cd71935a91 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowListItem.tsx @@ -0,0 +1,219 @@ +import { Badge, Button, Flex, Icon, IconButton, Image, Spacer, Text, Tooltip } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { $projectUrl } from 'app/store/nanostores/projectId'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectWorkflowId } from 'features/nodes/store/workflowSlice'; +import { useDeleteWorkflow } from 'features/workflowLibrary/components/DeleteLibraryWorkflowConfirmationAlertDialog'; +import { useLoadWorkflow } from 'features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog'; +import { useDownloadWorkflowById } from 'features/workflowLibrary/hooks/useDownloadWorkflowById'; +import type { MouseEvent } from 'react'; +import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + PiDownloadSimpleBold, + PiImageBold, + PiPencilBold, + PiShareFatBold, + PiTrashBold, + PiUsersBold, +} from 'react-icons/pi'; +import type { WorkflowRecordListItemWithThumbnailDTO } from 'services/api/types'; + +import { useShareWorkflow } from './ShareWorkflowModal'; + +const IMAGE_THUMBNAIL_SIZE = '80px'; +const FALLBACK_ICON_SIZE = '24px'; + +export const WorkflowListItem = ({ workflow }: { workflow: WorkflowRecordListItemWithThumbnailDTO }) => { + const { t } = useTranslation(); + const projectUrl = useStore($projectUrl); + const [isHovered, setIsHovered] = useState(false); + + const handleMouseOver = useCallback(() => { + setIsHovered(true); + }, []); + + const handleMouseOut = useCallback(() => { + setIsHovered(false); + }, []); + + const workflowId = useAppSelector(selectWorkflowId); + const downloadWorkflowById = useDownloadWorkflowById(); + const shareWorkflow = useShareWorkflow(); + const deleteWorkflow = useDeleteWorkflow(); + const loadWorkflow = useLoadWorkflow(); + + const isActive = useMemo(() => { + return workflowId === workflow.workflow_id; + }, [workflowId, workflow.workflow_id]); + + const handleClickLoad = useCallback(() => { + setIsHovered(false); + loadWorkflow.loadWithDialog(workflow.workflow_id, 'view'); + }, [loadWorkflow, workflow.workflow_id]); + + const handleClickEdit = useCallback(() => { + setIsHovered(false); + loadWorkflow.loadWithDialog(workflow.workflow_id, 'edit'); + }, [loadWorkflow, workflow.workflow_id]); + + const handleClickDelete = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + setIsHovered(false); + deleteWorkflow(workflow); + }, + [deleteWorkflow, workflow] + ); + + const handleClickShare = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + setIsHovered(false); + shareWorkflow(workflow); + }, + [shareWorkflow, workflow] + ); + + const handleClickDownload = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + setIsHovered(false); + downloadWorkflowById.downloadWorkflow(workflow.workflow_id); + }, + [downloadWorkflowById, workflow.workflow_id] + ); + + return ( + + + + + } + objectFit="cover" + objectPosition="50% 50%" + height={IMAGE_THUMBNAIL_SIZE} + width={IMAGE_THUMBNAIL_SIZE} + minHeight={IMAGE_THUMBNAIL_SIZE} + minWidth={IMAGE_THUMBNAIL_SIZE} + borderRadius="base" + /> + + + {workflow.name} + + {isActive && ( + + {t('workflows.opened')} + + )} + + + {workflow.description} + + + + + + + {workflow.category === 'project' && } + {workflow.category === 'default' && } + + + {workflow.category !== 'default' ? ( + + + } + /> + + + } + isLoading={downloadWorkflowById.isLoading} + /> + + {!!projectUrl && workflow.workflow_id && workflow.category !== 'user' && ( + + } + /> + + )} + + } + /> + + + ) : ( + + + + + )} + + + ); +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListItemTooltip.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowListItemTooltip.tsx similarity index 100% rename from invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowListItemTooltip.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowListItemTooltip.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowSearch.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSearch.tsx similarity index 66% rename from invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowSearch.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSearch.tsx index 145c48f29b..716c24269a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowSearch.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSearch.tsx @@ -1,4 +1,4 @@ -import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library'; +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 type { ChangeEvent, KeyboardEvent, RefObject } from 'react'; @@ -46,26 +46,28 @@ export const WorkflowSearch = memo(({ searchInputRef }: { searchInputRef: RefObj }, [searchInputRef]); return ( - - - {searchTerm && searchTerm.length && ( - - } - /> - - )} - + + + + {searchTerm && searchTerm.length && ( + + } + /> + + )} + + ); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowSortControl.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSortControl.tsx similarity index 55% rename from invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowSortControl.tsx rename to invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSortControl.tsx index c80d8f0b87..f43c5b3942 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/WorkflowSortControl.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowSortControl.tsx @@ -1,14 +1,4 @@ -import { - Flex, - FormControl, - FormLabel, - IconButton, - Popover, - PopoverBody, - PopoverContent, - PopoverTrigger, - Select, -} from '@invoke-ai/ui-library'; +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'; @@ -21,7 +11,6 @@ import { import type { ChangeEvent } from 'react'; import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiSortAscendingBold, PiSortDescendingBold } from 'react-icons/pi'; import { z } from 'zod'; const zOrderBy = z.enum(['opened_at', 'created_at', 'updated_at', 'name']); @@ -83,38 +72,23 @@ export const WorkflowSortControl = () => { const defaultOrderBy = projectId !== undefined ? 'opened_at' : 'created_at'; return ( - - - : } - variant="ghost" - /> - - - - - - - {t('common.orderBy')} - - - - {t('common.direction')} - - - - - - + + + {t('common.orderBy')} + + + + {t('common.direction')} + + + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/store/types.ts b/invokeai/frontend/web/src/features/nodes/store/types.ts index c5ee26665b..4f63cb72d4 100644 --- a/invokeai/frontend/web/src/features/nodes/store/types.ts +++ b/invokeai/frontend/web/src/features/nodes/store/types.ts @@ -22,10 +22,13 @@ export type NodesState = { export type WorkflowMode = 'edit' | 'view'; +export type WorkflowLibraryCategory = 'account' | 'private' | 'shared' | 'favorites' | `default`; + export type WorkflowsState = Omit & { _version: 1; isTouched: boolean; mode: WorkflowMode; + browsingCategory: WorkflowLibraryCategory; searchTerm: string; orderBy?: WorkflowRecordOrderBy; orderDirection: SQLiteDirection; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowLibraryModal.ts b/invokeai/frontend/web/src/features/nodes/store/workflowLibraryModal.ts new file mode 100644 index 0000000000..bf490f67dc --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/store/workflowLibraryModal.ts @@ -0,0 +1,6 @@ +import { buildUseDisclosure } from 'common/hooks/useBoolean'; + +/** + * Tracks the state for the workflow library modal. + */ +export const [useWorkflowLibraryModal, $isWorkflowLibraryModalOpen] = buildUseDisclosure(false); diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowListMenu.ts b/invokeai/frontend/web/src/features/nodes/store/workflowListMenu.ts deleted file mode 100644 index cfc182046c..0000000000 --- a/invokeai/frontend/web/src/features/nodes/store/workflowListMenu.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { buildUseDisclosure } from 'common/hooks/useBoolean'; - -/** - * Tracks the state for the workflow list menu. - */ -export const [useWorkflowListMenu, $isWorkflowListMenuIsOpen] = buildUseDisclosure(false); diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 7dce3bfb26..2b974e9275 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -11,7 +11,12 @@ import { } from 'features/nodes/components/sidePanel/builder/form-manipulation'; import { workflowLoaded } from 'features/nodes/store/actions'; import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged } from 'features/nodes/store/nodesSlice'; -import type { NodesState, WorkflowMode, WorkflowsState as WorkflowState } from 'features/nodes/store/types'; +import type { + NodesState, + WorkflowLibraryCategory, + WorkflowMode, + WorkflowsState as WorkflowState, +} from 'features/nodes/store/types'; import type { FieldIdentifier, StatefulFieldValue } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; import type { @@ -82,6 +87,7 @@ const initialWorkflowState: WorkflowState = { orderBy: undefined, // initial value is decided in component orderDirection: 'DESC', categorySections: {}, + browsingCategory: 'account', ...getBlankWorkflow(), }; @@ -101,6 +107,10 @@ export const workflowSlice = createSlice({ workflowOrderDirectionChanged: (state, action: PayloadAction) => { state.orderDirection = action.payload; }, + workflowBrowsingCategoryChanged: (state, action: PayloadAction) => { + state.browsingCategory = action.payload; + state.searchTerm = ''; + }, categorySectionsChanged: (state, action: PayloadAction<{ id: string; isOpen: boolean }>) => { const { id, isOpen } = action.payload; state.categorySections[id] = isOpen; @@ -299,6 +309,7 @@ export const { workflowSearchTermChanged, workflowOrderByChanged, workflowOrderDirectionChanged, + workflowBrowsingCategoryChanged, categorySectionsChanged, formReset, formElementAdded, @@ -365,6 +376,7 @@ export const selectWorkflowIsTouched = createWorkflowSelector((workflow) => work export const selectWorkflowSearchTerm = createWorkflowSelector((workflow) => workflow.searchTerm); export const selectWorkflowOrderBy = createWorkflowSelector((workflow) => workflow.orderBy); export const selectWorkflowOrderDirection = createWorkflowSelector((workflow) => workflow.orderDirection); +export const selectWorkflowBrowsingCategory = createWorkflowSelector((workflow) => workflow.browsingCategory); export const selectWorkflowDescription = createWorkflowSelector((workflow) => workflow.description); export const selectWorkflowForm = createWorkflowSelector((workflow) => workflow.form); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog.tsx index 1f639a9bc8..9deda79605 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/LoadWorkflowConfirmationAlertDialog.tsx @@ -2,7 +2,7 @@ import { ConfirmationAlertDialog, Flex, Text } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; -import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu'; +import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal'; import { selectWorkflowIsTouched, workflowModeChanged } from 'features/nodes/store/workflowSlice'; import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow'; import { atom } from 'nanostores'; @@ -14,7 +14,7 @@ const cleanup = () => $workflowToLoad.set(null); export const useLoadWorkflow = () => { const dispatch = useAppDispatch(); - const workflowListMenu = useWorkflowListMenu(); + const workflowLibraryModal = useWorkflowLibraryModal(); const { getAndLoadWorkflow } = useGetAndLoadLibraryWorkflow(); const isTouched = useAppSelector(selectWorkflowIsTouched); @@ -28,8 +28,8 @@ export const useLoadWorkflow = () => { await getAndLoadWorkflow(workflowId); dispatch(workflowModeChanged(mode)); cleanup(); - workflowListMenu.close(); - }, [dispatch, getAndLoadWorkflow, workflowListMenu]); + workflowLibraryModal.close(); + }, [dispatch, getAndLoadWorkflow, workflowLibraryModal]); const loadWithDialog = useCallback( (workflowId: string, mode: 'view' | 'edit') => { diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/UploadWorkflowButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/UploadWorkflowButton.tsx index 1695206041..d41e0f2984 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/UploadWorkflowButton.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/UploadWorkflowButton.tsx @@ -1,5 +1,5 @@ import { IconButton } from '@invoke-ai/ui-library'; -import { useWorkflowListMenu } from 'features/nodes/store/workflowListMenu'; +import { useWorkflowLibraryModal } from 'features/nodes/store/workflowLibraryModal'; import { saveWorkflowAs } from 'features/workflowLibrary/components/SaveWorkflowAsDialog'; import { useLoadWorkflowFromFile } from 'features/workflowLibrary/hooks/useLoadWorkflowFromFile'; import { memo, useCallback, useRef } from 'react'; @@ -10,12 +10,12 @@ import { PiUploadSimpleBold } from 'react-icons/pi'; const UploadWorkflowButton = () => { const { t } = useTranslation(); const resetRef = useRef<() => void>(null); - const workflowListMenu = useWorkflowListMenu(); + const workflowLibraryModal = useWorkflowLibraryModal(); const loadWorkflowFromFile = useLoadWorkflowFromFile({ resetRef, onSuccess: (workflow) => { - workflowListMenu.close(); + workflowLibraryModal.close(); saveWorkflowAs(workflow); }, }); diff --git a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts index 23d4763f85..31650170db 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/workflows.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/workflows.ts @@ -1,6 +1,7 @@ import type { paths } from 'services/api/schema'; import { api, buildV1Url, LIST_TAG } from '..'; +import queryString from 'query-string'; /** * Builds an endpoint URL for the workflows router @@ -73,8 +74,7 @@ export const workflowsApi = api.injectEndpoints({ NonNullable >({ query: (params) => ({ - url: buildWorkflowsUrl(), - params, + url: `${buildWorkflowsUrl()}?${queryString.stringify(params, { arrayFormat: 'none' })}`, }), providesTags: ['FetchOnReconnect', { type: 'Workflow', id: LIST_TAG }], }), diff --git a/invokeai/frontend/web/src/services/api/schema.ts b/invokeai/frontend/web/src/services/api/schema.ts index 926381bde9..714250405b 100644 --- a/invokeai/frontend/web/src/services/api/schema.ts +++ b/invokeai/frontend/web/src/services/api/schema.ts @@ -24252,8 +24252,8 @@ export interface operations { order_by?: components["schemas"]["WorkflowRecordOrderBy"]; /** @description The direction to order by */ direction?: components["schemas"]["SQLiteDirection"]; - /** @description The category of workflow to get */ - category?: components["schemas"]["WorkflowCategory"]; + /** @description The categories of workflow to get */ + categories?: components["schemas"]["WorkflowCategory"][] | null; /** @description The text to query by (matches name and description) */ query?: string | null; };