mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat: workflow library WIP
- Save to library - Duplicate - Filter/sort - UI/queries
This commit is contained in:
parent
46905175a9
commit
0a25efd054
@ -1,12 +1,16 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Body, HTTPException, Path, Query
|
||||
|
||||
from invokeai.app.api.dependencies import ApiDependencies
|
||||
from invokeai.app.services.shared.pagination import PaginatedResults
|
||||
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
|
||||
from invokeai.app.services.workflow_records.workflow_records_common import (
|
||||
Workflow,
|
||||
WorkflowNotFoundError,
|
||||
WorkflowRecordDTO,
|
||||
WorkflowRecordListItemDTO,
|
||||
WorkflowRecordOrderBy,
|
||||
WorkflowWithoutID,
|
||||
)
|
||||
|
||||
@ -79,6 +83,11 @@ async def create_workflow(
|
||||
async def list_workflows(
|
||||
page: int = Query(default=0, description="The page to get"),
|
||||
per_page: int = Query(default=10, description="The number of workflows per page"),
|
||||
order_by: WorkflowRecordOrderBy = Query(default=WorkflowRecordOrderBy.Name, description="The order by"),
|
||||
direction: SQLiteDirection = Query(default=SQLiteDirection.Ascending, description="The order by"),
|
||||
filter_text: Optional[str] = Query(default=None, description="The name to filter by"),
|
||||
) -> PaginatedResults[WorkflowRecordListItemDTO]:
|
||||
"""Gets a page of workflows"""
|
||||
return ApiDependencies.invoker.services.workflow_records.get_many(page=page, per_page=per_page)
|
||||
return ApiDependencies.invoker.services.workflow_records.get_many(
|
||||
page=page, per_page=per_page, order_by=order_by, direction=direction, filter_text=filter_text
|
||||
)
|
||||
|
@ -1 +1,10 @@
|
||||
from enum import Enum
|
||||
|
||||
from invokeai.app.util.metaenum import MetaEnum
|
||||
|
||||
sqlite_memory = ":memory:"
|
||||
|
||||
|
||||
class SQLiteDirection(str, Enum, metaclass=MetaEnum):
|
||||
Ascending = "ASC"
|
||||
Descending = "DESC"
|
||||
|
@ -1,10 +1,13 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
from invokeai.app.services.shared.pagination import PaginatedResults
|
||||
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
|
||||
from invokeai.app.services.workflow_records.workflow_records_common import (
|
||||
Workflow,
|
||||
WorkflowRecordDTO,
|
||||
WorkflowRecordListItemDTO,
|
||||
WorkflowRecordOrderBy,
|
||||
WorkflowWithoutID,
|
||||
)
|
||||
|
||||
@ -33,6 +36,13 @@ class WorkflowRecordsStorageBase(ABC):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_many(self, page: int, per_page: int) -> PaginatedResults[WorkflowRecordListItemDTO]:
|
||||
def get_many(
|
||||
self,
|
||||
page: int,
|
||||
per_page: int,
|
||||
order_by: WorkflowRecordOrderBy,
|
||||
direction: SQLiteDirection,
|
||||
filter_text: Optional[str],
|
||||
) -> PaginatedResults[WorkflowRecordListItemDTO]:
|
||||
"""Gets many workflows."""
|
||||
pass
|
||||
|
@ -1,9 +1,11 @@
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Union
|
||||
|
||||
import semver
|
||||
from pydantic import BaseModel, Field, JsonValue, TypeAdapter, field_validator
|
||||
|
||||
from invokeai.app.util.metaenum import MetaEnum
|
||||
from invokeai.app.util.misc import uuid_string
|
||||
|
||||
__workflow_meta_version__ = semver.Version.parse("1.0.0")
|
||||
@ -57,8 +59,10 @@ WorkflowValidator = TypeAdapter(Workflow)
|
||||
class WorkflowRecordDTO(BaseModel):
|
||||
workflow_id: str = Field(description="The id of the workflow.")
|
||||
workflow: Workflow = Field(description="The workflow.")
|
||||
name: str = Field(description="The name of the workflow.")
|
||||
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the workflow.")
|
||||
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the workflow.")
|
||||
opened_at: Union[datetime.datetime, str] = Field(description="The opened timestamp of the workflow.")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "WorkflowRecordDTO":
|
||||
@ -75,6 +79,7 @@ class WorkflowRecordListItemDTO(BaseModel):
|
||||
description: str = Field(description="The description of the workflow.")
|
||||
created_at: Union[datetime.datetime, str] = Field(description="The created timestamp of the workflow.")
|
||||
updated_at: Union[datetime.datetime, str] = Field(description="The updated timestamp of the workflow.")
|
||||
opened_at: Union[datetime.datetime, str] = Field(description="The opened timestamp of the workflow.")
|
||||
|
||||
|
||||
WorkflowRecordListItemDTOValidator = TypeAdapter(WorkflowRecordListItemDTO)
|
||||
@ -82,3 +87,12 @@ WorkflowRecordListItemDTOValidator = TypeAdapter(WorkflowRecordListItemDTO)
|
||||
|
||||
class WorkflowNotFoundError(Exception):
|
||||
"""Raised when a workflow is not found"""
|
||||
|
||||
|
||||
class WorkflowRecordOrderBy(str, Enum, metaclass=MetaEnum):
|
||||
"""The order by options for workflow records"""
|
||||
|
||||
CreatedAt = "created_at"
|
||||
UpdatedAt = "updated_at"
|
||||
OpenedAt = "opened_at"
|
||||
Name = "name"
|
||||
|
@ -1,5 +1,8 @@
|
||||
from typing import Optional
|
||||
|
||||
from invokeai.app.services.invoker import Invoker
|
||||
from invokeai.app.services.shared.pagination import PaginatedResults
|
||||
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
|
||||
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
|
||||
from invokeai.app.services.workflow_records.workflow_records_base import WorkflowRecordsStorageBase
|
||||
from invokeai.app.services.workflow_records.workflow_records_common import (
|
||||
@ -8,6 +11,7 @@ from invokeai.app.services.workflow_records.workflow_records_common import (
|
||||
WorkflowRecordDTO,
|
||||
WorkflowRecordListItemDTO,
|
||||
WorkflowRecordListItemDTOValidator,
|
||||
WorkflowRecordOrderBy,
|
||||
WorkflowValidator,
|
||||
WorkflowWithoutID,
|
||||
)
|
||||
@ -25,11 +29,20 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
|
||||
self._invoker = invoker
|
||||
|
||||
def get(self, workflow_id: str) -> WorkflowRecordDTO:
|
||||
"""Gets a workflow by ID. Updates the opened_at column."""
|
||||
try:
|
||||
self._lock.acquire()
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
SELECT workflow_id, workflow, created_at, updated_at
|
||||
UPDATE workflow_library
|
||||
SET opened_at = STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')
|
||||
WHERE workflow_id = ?;
|
||||
""",
|
||||
(workflow_id,),
|
||||
)
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
SELECT workflow_id, workflow, name, created_at, updated_at, opened_at
|
||||
FROM workflow_library
|
||||
WHERE workflow_id = ?;
|
||||
""",
|
||||
@ -107,34 +120,50 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
|
||||
self._lock.release()
|
||||
return None
|
||||
|
||||
def get_many(self, page: int, per_page: int) -> PaginatedResults[WorkflowRecordListItemDTO]:
|
||||
def get_many(
|
||||
self,
|
||||
page: int,
|
||||
per_page: int,
|
||||
order_by: WorkflowRecordOrderBy,
|
||||
direction: SQLiteDirection,
|
||||
filter_text: Optional[str] = None,
|
||||
) -> PaginatedResults[WorkflowRecordListItemDTO]:
|
||||
try:
|
||||
self._lock.acquire()
|
||||
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
# sanitize!
|
||||
assert order_by in WorkflowRecordOrderBy
|
||||
assert direction in SQLiteDirection
|
||||
count_query = "SELECT COUNT(*) FROM workflow_library"
|
||||
main_query = """
|
||||
SELECT
|
||||
workflow_id,
|
||||
json_extract(workflow, '$.name') AS name,
|
||||
json_extract(workflow, '$.description') AS description,
|
||||
name,
|
||||
description,
|
||||
created_at,
|
||||
updated_at
|
||||
updated_at,
|
||||
opened_at
|
||||
FROM workflow_library
|
||||
ORDER BY name ASC
|
||||
LIMIT ? OFFSET ?;
|
||||
""",
|
||||
(per_page, page * per_page),
|
||||
)
|
||||
"""
|
||||
main_params = []
|
||||
count_params = []
|
||||
stripped_filter_name = filter_text.strip() if filter_text else None
|
||||
if stripped_filter_name:
|
||||
filter_string = "%" + stripped_filter_name + "%"
|
||||
main_query += " WHERE name LIKE ? OR description LIKE ? "
|
||||
count_query += " WHERE name LIKE ? OR description LIKE ?;"
|
||||
main_params.extend([filter_string, filter_string])
|
||||
count_params.extend([filter_string, filter_string])
|
||||
|
||||
main_query += f" ORDER BY {order_by.value} {direction.value} LIMIT ? OFFSET ?;"
|
||||
main_params.extend([per_page, page * per_page])
|
||||
self._cursor.execute(main_query, main_params)
|
||||
rows = self._cursor.fetchall()
|
||||
workflows = [WorkflowRecordListItemDTOValidator.validate_python(dict(row)) for row in rows]
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
SELECT COUNT(*)
|
||||
FROM workflow_library;
|
||||
"""
|
||||
)
|
||||
|
||||
self._cursor.execute(count_query, count_params)
|
||||
total = self._cursor.fetchone()[0]
|
||||
pages = int(total / per_page) + 1
|
||||
|
||||
return PaginatedResults(
|
||||
items=workflows,
|
||||
page=page,
|
||||
@ -154,10 +183,16 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
CREATE TABLE IF NOT EXISTS workflow_library (
|
||||
workflow_id TEXT NOT NULL PRIMARY KEY, -- gets implicit index
|
||||
workflow_id TEXT NOT NULL PRIMARY KEY,
|
||||
workflow TEXT NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||
updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')) -- updated via trigger
|
||||
-- updated via trigger
|
||||
updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||
-- updated manually when retrieving workflow
|
||||
opened_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||
-- Generated columns, needed for indexing and searching
|
||||
name TEXT GENERATED ALWAYS as (json_extract(workflow, '$.name')) VIRTUAL NOT NULL,
|
||||
description TEXT GENERATED ALWAYS as (json_extract(workflow, '$.description')) VIRTUAL NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
@ -175,6 +210,32 @@ class SqliteWorkflowRecordsStorage(WorkflowRecordsStorageBase):
|
||||
"""
|
||||
)
|
||||
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_library_created_at ON workflow_library(created_at);
|
||||
"""
|
||||
)
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_library_updated_at ON workflow_library(updated_at);
|
||||
"""
|
||||
)
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_library_opened_at ON workflow_library(opened_at);
|
||||
"""
|
||||
)
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_library_name ON workflow_library(name);
|
||||
"""
|
||||
)
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
CREATE INDEX IF NOT EXISTS idx_workflow_library_description ON workflow_library(description);
|
||||
"""
|
||||
)
|
||||
|
||||
# We do not need the original `workflows` table or `workflow_images` junction table.
|
||||
self._cursor.execute(
|
||||
"""--sql
|
||||
|
@ -69,6 +69,7 @@
|
||||
"data": "Data",
|
||||
"delete": "Delete",
|
||||
"details": "Details",
|
||||
"direction": "Direction",
|
||||
"ipAdapter": "IP Adapter",
|
||||
"t2iAdapter": "T2I Adapter",
|
||||
"darkMode": "Dark Mode",
|
||||
@ -104,7 +105,6 @@
|
||||
"langSpanish": "Español",
|
||||
"languagePickerLabel": "Language",
|
||||
"langUkranian": "Украї́нська",
|
||||
"lastUpdated": "Last updated: {{date}}",
|
||||
"lightMode": "Light Mode",
|
||||
"linear": "Linear",
|
||||
"load": "Load",
|
||||
@ -117,6 +117,7 @@
|
||||
"nodesDesc": "A node based system for the generation of images is under development currently. Stay tuned for updates about this amazing feature.",
|
||||
"notInstalled": "Not $t(common.installed)",
|
||||
"openInNewTab": "Open in New Tab",
|
||||
"orderBy": "Order By",
|
||||
"outpaint": "outpaint",
|
||||
"outputs": "Outputs",
|
||||
"postProcessDesc1": "Invoke AI offers a wide variety of post processing features. Image Upscaling and Face Restoration are already available in the WebUI. You can access them from the Advanced Options menu of the Text To Image and Image To Image tabs. You can also process images directly, using the image action buttons above the current image display or in the viewer.",
|
||||
@ -164,6 +165,8 @@
|
||||
"unifiedCanvas": "Unified Canvas",
|
||||
"unknown": "Unknown",
|
||||
"upload": "Upload",
|
||||
"updated": "Updated",
|
||||
"created": "Created",
|
||||
"prevPage": "Previous Page",
|
||||
"nextPage": "Next Page"
|
||||
},
|
||||
@ -1608,13 +1611,30 @@
|
||||
"undo": "Undo"
|
||||
},
|
||||
"workflows": {
|
||||
"workflows": "Workflows",
|
||||
"workflowLibrary": "Workflow Library",
|
||||
"userCategory": "User",
|
||||
"systemCategory": "System",
|
||||
"loadWorkflow": "$t(nodes.loadWorkflow)",
|
||||
"user": "User",
|
||||
"system": "System",
|
||||
"recent": "Recent",
|
||||
"userWorkflows": "$t(workflows.user) $t(workflows.workflows)",
|
||||
"recentWorkflows": "$t(workflows.recent) $t(workflows.workflows)",
|
||||
"systemWorkflows": "$t(workflows.system) $t(workflows.workflows)",
|
||||
"openWorkflow": "Open Workflow",
|
||||
"uploadWorkflow": "Upload Workflow",
|
||||
"deleteWorkflow": "Delete Workflow",
|
||||
"unnamedWorkflow": "Unnamed Workflow",
|
||||
"downloadWorkflow": "Download Workflow",
|
||||
"saveWorkflow": "Save Workflow"
|
||||
"saveWorkflow": "Save Workflow",
|
||||
"duplicateWorkflow": "Duplicate Workflow",
|
||||
"problemSavingWorkflow": "Problem Saving Workflow",
|
||||
"workflowSaved": "Workflow Saved",
|
||||
"noRecentWorkflows": "No Recent Workflows",
|
||||
"noUserWorkflows": "No User Workflows",
|
||||
"noSystemWorkflows": "No System Workflows",
|
||||
"problemLoading": "Problem Loading Workflows",
|
||||
"loading": "Loading Workflows",
|
||||
"noDescription": "No description",
|
||||
"searchWorkflows": "Search Workflows",
|
||||
"clearWorkflowSearchFilter": "Clear Workflow Search Filter"
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,10 @@
|
||||
import { FormControl, FormLabel, Tooltip, forwardRef } from '@chakra-ui/react';
|
||||
import {
|
||||
FormControl,
|
||||
FormControlProps,
|
||||
FormLabel,
|
||||
Tooltip,
|
||||
forwardRef,
|
||||
} from '@chakra-ui/react';
|
||||
import { Select, SelectProps } from '@mantine/core';
|
||||
import { useMantineSelectStyles } from 'mantine-theme/hooks/useMantineSelectStyles';
|
||||
import { RefObject, memo } from 'react';
|
||||
@ -13,10 +19,19 @@ export type IAISelectProps = Omit<SelectProps, 'label'> & {
|
||||
tooltip?: string | null;
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
label?: string;
|
||||
formControlProps?: FormControlProps;
|
||||
};
|
||||
|
||||
const IAIMantineSelect = forwardRef((props: IAISelectProps, ref) => {
|
||||
const { tooltip, inputRef, label, disabled, required, ...rest } = props;
|
||||
const {
|
||||
tooltip,
|
||||
formControlProps,
|
||||
inputRef,
|
||||
label,
|
||||
disabled,
|
||||
required,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const styles = useMantineSelectStyles();
|
||||
|
||||
@ -28,6 +43,7 @@ const IAIMantineSelect = forwardRef((props: IAISelectProps, ref) => {
|
||||
isDisabled={disabled}
|
||||
position="static"
|
||||
data-testid={`select-${label || props.placeholder}`}
|
||||
{...formControlProps}
|
||||
>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<Select disabled={disabled} ref={inputRef} styles={styles} {...rest} />
|
||||
|
1
invokeai/frontend/web/src/common/components/Nbsp.tsx
Normal file
1
invokeai/frontend/web/src/common/components/Nbsp.tsx
Normal file
@ -0,0 +1 @@
|
||||
export const Nbsp = () => <>{'\u00A0'}</>;
|
@ -1,40 +0,0 @@
|
||||
import { ButtonGroup, Flex } from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import { WorkflowCategory } from './types';
|
||||
import { Dispatch, SetStateAction, memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type Props = {
|
||||
category: WorkflowCategory;
|
||||
setCategory: Dispatch<SetStateAction<WorkflowCategory>>;
|
||||
};
|
||||
|
||||
const WorkflowLibraryCategories = ({ category, setCategory }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const handleClickUser = useCallback(() => {
|
||||
setCategory('user');
|
||||
}, [setCategory]);
|
||||
const handleClickSystem = useCallback(() => {
|
||||
setCategory('system');
|
||||
}, [setCategory]);
|
||||
return (
|
||||
<Flex layerStyle="second" p={2} borderRadius="base">
|
||||
<ButtonGroup orientation="vertical">
|
||||
<IAIButton
|
||||
onClick={handleClickUser}
|
||||
variant={category === 'user' ? 'invokeAI' : 'ghost'}
|
||||
>
|
||||
{t('workflows.userCategory')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
onClick={handleClickSystem}
|
||||
variant={category === 'system' ? 'invokeAI' : 'ghost'}
|
||||
>
|
||||
{t('workflows.systemCategory')}
|
||||
</IAIButton>
|
||||
</ButtonGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryCategories);
|
@ -1,37 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { memo, useState } from 'react';
|
||||
import { useListWorkflowsQuery } from 'services/api/endpoints/workflows';
|
||||
import WorkflowLibraryCategories from './WorkflowLibraryCategories';
|
||||
import WorkflowLibraryList from './WorkflowLibraryList';
|
||||
import WorkflowLibraryPagination from './WorkflowLibraryPagination';
|
||||
import { WorkflowCategory } from './types';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
const WorkflowLibraryContent = () => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [category, setCategory] = useState<WorkflowCategory>('user');
|
||||
const { data } = useListWorkflowsQuery({
|
||||
page,
|
||||
per_page: PER_PAGE,
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex w="full" h="full" gap={2}>
|
||||
<WorkflowLibraryCategories
|
||||
category={category}
|
||||
setCategory={setCategory}
|
||||
/>
|
||||
<Flex h="full" w="full" gap={2} flexDir="column">
|
||||
<WorkflowLibraryList data={data} />
|
||||
<WorkflowLibraryPagination data={data} page={page} setPage={setPage} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryContent);
|
@ -1,25 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import WorkflowLibraryWorkflowItem from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryWorkflowItem';
|
||||
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||
import { memo } from 'react';
|
||||
import { paths } from 'services/api/schema';
|
||||
|
||||
type Props = {
|
||||
data: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'];
|
||||
};
|
||||
|
||||
const WorkflowLibraryList = ({ data }: Props) => {
|
||||
return (
|
||||
<Flex w="full" h="full" layerStyle="second" p={2} borderRadius="base">
|
||||
<ScrollableContent>
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
{data.items.map((w) => (
|
||||
<WorkflowLibraryWorkflowItem key={w.workflow_id} workflowDTO={w} />
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryList);
|
@ -1,69 +0,0 @@
|
||||
import { Flex, Heading, Spacer, Text } from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import dateFormat from 'dateformat';
|
||||
import { useWorkflowLibraryContext } from 'features/nodes/components/flow/WorkflowLibrary/useWorkflowLibraryContext';
|
||||
import { useDeleteLibraryWorkflow } from 'features/nodes/hooks/useDeleteLibraryWorkflow';
|
||||
import { useGetAndLoadLibraryWorkflow } from 'features/nodes/hooks/useGetAndLoadLibraryWorkflow';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { paths } from 'services/api/schema';
|
||||
|
||||
type Props = {
|
||||
workflowDTO: paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json']['items'][number];
|
||||
};
|
||||
|
||||
const WorkflowLibraryList = ({ workflowDTO }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { onClose } = useWorkflowLibraryContext();
|
||||
const { deleteWorkflow, deleteWorkflowResult } = useDeleteLibraryWorkflow({});
|
||||
const { getAndLoadWorkflow, getAndLoadWorkflowResult } =
|
||||
useGetAndLoadLibraryWorkflow({ onSuccess: onClose });
|
||||
|
||||
const handleDeleteWorkflow = useCallback(() => {
|
||||
deleteWorkflow(workflowDTO.workflow_id);
|
||||
}, [deleteWorkflow, workflowDTO.workflow_id]);
|
||||
|
||||
const handleGetAndLoadWorkflow = useCallback(() => {
|
||||
getAndLoadWorkflow(workflowDTO.workflow_id);
|
||||
}, [getAndLoadWorkflow, workflowDTO.workflow_id]);
|
||||
|
||||
return (
|
||||
<Flex key={workflowDTO.workflow_id} w="full">
|
||||
<Flex w="full" alignItems="center" gap={2}>
|
||||
<Flex flexDir="column" flexGrow={1}>
|
||||
<Flex alignItems="center" w="full">
|
||||
<Heading size="sm">
|
||||
{workflowDTO.name || t('workflows.unnamedWorkflow')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Text fontSize="sm" fontStyle="italic" variant="subtext">
|
||||
{t('common.lastUpdated', {
|
||||
date: dateFormat(workflowDTO.updated_at),
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text fontSize="sm" noOfLines={1}>
|
||||
{workflowDTO.description}
|
||||
</Text>
|
||||
</Flex>
|
||||
<IAIButton
|
||||
onClick={handleGetAndLoadWorkflow}
|
||||
isLoading={getAndLoadWorkflowResult.isLoading}
|
||||
aria-label={t('workflows.loadWorkflow')}
|
||||
>
|
||||
{t('common.load')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
colorScheme="error"
|
||||
onClick={handleDeleteWorkflow}
|
||||
isLoading={deleteWorkflowResult.isLoading}
|
||||
aria-label={t('workflows.deleteWorkflow')}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</IAIButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryList);
|
@ -1 +0,0 @@
|
||||
export type WorkflowCategory = 'user' | 'system';
|
@ -1,12 +0,0 @@
|
||||
import { WorkflowLibraryContext } from 'features/nodes/components/flow/WorkflowLibrary/context';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useWorkflowLibraryContext = () => {
|
||||
const context = useContext(WorkflowLibraryContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useWorkflowLibraryContext must be used within a WorkflowLibraryContext.Provider'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import { FileButton } from '@mantine/core';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useLoadWorkflowFromFile } from 'features/nodes/hooks/useLoadWorkflowFromFile';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaSave, FaUpload } from 'react-icons/fa';
|
||||
|
||||
const LoadWorkflowButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const resetRef = useRef<() => void>(null);
|
||||
const loadWorkflowFromFile = useLoadWorkflowFromFile(resetRef);
|
||||
return (
|
||||
<FileButton
|
||||
resetRef={resetRef}
|
||||
accept="application/json"
|
||||
onChange={loadWorkflowFromFile}
|
||||
>
|
||||
{(props) => (
|
||||
<IAIIconButton
|
||||
icon={<FaSave />}
|
||||
tooltip={t('nodes.loadWorkflow')}
|
||||
aria-label={t('nodes.loadWorkflow')}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
</FileButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LoadWorkflowButton);
|
@ -1,9 +1,10 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import { memo } from 'react';
|
||||
import DownloadWorkflowButton from './DownloadWorkflowButton';
|
||||
import LoadWorkflowButton from './LoadWorkflowButton';
|
||||
import ResetWorkflowButton from './ResetWorkflowButton';
|
||||
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopCenterPanel/SaveWorkflowButton';
|
||||
import DownloadWorkflowButton from 'features/workflowLibrary/components/DownloadWorkflowButton';
|
||||
import UploadWorkflowButton from 'features/workflowLibrary/components/LoadWorkflowFromFileButton';
|
||||
import ResetWorkflowButton from 'features/workflowLibrary/components/ResetWorkflowButton';
|
||||
import SaveWorkflowButton from 'features/workflowLibrary/components/SaveWorkflowButton';
|
||||
import DuplicateWorkflowButton from 'features/workflowLibrary/components/DuplicateWorkflowButton';
|
||||
|
||||
const TopCenterPanel = () => {
|
||||
return (
|
||||
@ -17,8 +18,9 @@ const TopCenterPanel = () => {
|
||||
}}
|
||||
>
|
||||
<DownloadWorkflowButton />
|
||||
<LoadWorkflowButton />
|
||||
<UploadWorkflowButton />
|
||||
<SaveWorkflowButton />
|
||||
<DuplicateWorkflowButton />
|
||||
<ResetWorkflowButton />
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,53 +1,13 @@
|
||||
import {
|
||||
Flex,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
} from '@chakra-ui/react';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import WorkflowLibraryButton from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryButton';
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton';
|
||||
import { memo } from 'react';
|
||||
import { FaEllipsis } from 'react-icons/fa6';
|
||||
import WorkflowEditorSettings from './WorkflowEditorSettings';
|
||||
import { MENU_LIST_MOTION_PROPS as MENU_LIST_MOTION_PROPS } from 'theme/components/menu';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDownloadWorkflow } from 'features/nodes/hooks/useDownloadWorkflow';
|
||||
import { FaDownload, FaSave } from 'react-icons/fa';
|
||||
import { useSaveWorkflow } from 'features/nodes/hooks/useSaveWorkflow';
|
||||
|
||||
const TopRightPanel = () => {
|
||||
const { t } = useTranslation();
|
||||
const downloadWorkflow = useDownloadWorkflow();
|
||||
const saveWorkflow = useSaveWorkflow();
|
||||
return (
|
||||
<Flex sx={{ gap: 2, position: 'absolute', top: 2, insetInlineEnd: 2 }}>
|
||||
<WorkflowEditorSettings />
|
||||
<WorkflowLibraryButton />
|
||||
<Menu>
|
||||
<MenuButton as={IAIIconButton} icon={<FaEllipsis />} />
|
||||
<MenuList motionProps={MENU_LIST_MOTION_PROPS}>
|
||||
<MenuItem onClick={saveWorkflow} icon={<FaSave />}>
|
||||
{t('workflows.saveWorkflow')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={downloadWorkflow} icon={<FaDownload />}>
|
||||
{t('workflows.downloadWorkflow')}
|
||||
</MenuItem>
|
||||
{/* <MenuGroup title={t('common.settingsLabel')}>
|
||||
<HotkeysModal>
|
||||
<MenuItem as="button" icon={<FaKeyboard />}>
|
||||
{t('common.hotkeysLabel')}
|
||||
</MenuItem>
|
||||
</HotkeysModal>
|
||||
<SettingsModal>
|
||||
<MenuItem as="button" icon={<FaCog />}>
|
||||
{t('common.settingsLabel')}
|
||||
</MenuItem>
|
||||
</SettingsModal>
|
||||
</MenuGroup> */}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<WorkflowEditorSettings />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -19,8 +19,8 @@ const DownloadWorkflowButton = () => {
|
||||
return (
|
||||
<IAIIconButton
|
||||
icon={<FaDownload />}
|
||||
tooltip={t('nodes.downloadWorkflow')}
|
||||
aria-label={t('nodes.downloadWorkflow')}
|
||||
tooltip={t('workflows.downloadWorkflow')}
|
||||
aria-label={t('workflows.downloadWorkflow')}
|
||||
onClick={handleDownload}
|
||||
/>
|
||||
);
|
@ -0,0 +1,21 @@
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useDuplicateLibraryWorkflow } from 'features/workflowLibrary/hooks/useDuplicateWorkflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaClone } from 'react-icons/fa';
|
||||
|
||||
const DuplicateLibraryWorkflowButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { duplicateWorkflow, isLoading } = useDuplicateLibraryWorkflow();
|
||||
return (
|
||||
<IAIIconButton
|
||||
icon={<FaClone />}
|
||||
onClick={duplicateWorkflow}
|
||||
isLoading={isLoading}
|
||||
tooltip={t('workflows.duplicateWorkflow')}
|
||||
aria-label={t('workflows.duplicateWorkflow')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DuplicateLibraryWorkflowButton);
|
@ -1,11 +1,11 @@
|
||||
import { FileButton } from '@mantine/core';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useLoadWorkflowFromFile } from 'features/nodes/hooks/useLoadWorkflowFromFile';
|
||||
import { useLoadWorkflowFromFile } from 'features/workflowLibrary/hooks/useLoadWorkflowFromFile';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaUpload } from 'react-icons/fa';
|
||||
|
||||
const LoadWorkflowButton = () => {
|
||||
const UploadWorkflowButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const resetRef = useRef<() => void>(null);
|
||||
const loadWorkflowFromFile = useLoadWorkflowFromFile(resetRef);
|
||||
@ -18,8 +18,8 @@ const LoadWorkflowButton = () => {
|
||||
{(props) => (
|
||||
<IAIIconButton
|
||||
icon={<FaUpload />}
|
||||
tooltip={t('nodes.loadWorkflow')}
|
||||
aria-label={t('nodes.loadWorkflow')}
|
||||
tooltip={t('workflows.uploadWorkflow')}
|
||||
aria-label={t('workflows.uploadWorkflow')}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
@ -27,4 +27,4 @@ const LoadWorkflowButton = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LoadWorkflowButton);
|
||||
export default memo(UploadWorkflowButton);
|
@ -10,8 +10,7 @@ import {
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { RootState } from 'app/store/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { nodeEditorReset } from 'features/nodes/store/nodesSlice';
|
||||
import { addToast } from 'features/system/store/systemSlice';
|
||||
@ -26,10 +25,6 @@ const ResetWorkflowButton = () => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const cancelRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const nodesCount = useAppSelector(
|
||||
(state: RootState) => state.nodes.nodes.length
|
||||
);
|
||||
|
||||
const handleConfirmClear = useCallback(() => {
|
||||
dispatch(nodeEditorReset());
|
||||
|
||||
@ -52,7 +47,6 @@ const ResetWorkflowButton = () => {
|
||||
tooltip={t('nodes.resetWorkflow')}
|
||||
aria-label={t('nodes.resetWorkflow')}
|
||||
onClick={onOpen}
|
||||
isDisabled={!nodesCount}
|
||||
colorScheme="error"
|
||||
/>
|
||||
|
@ -0,0 +1,21 @@
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaSave } from 'react-icons/fa';
|
||||
|
||||
const SaveLibraryWorkflowButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { saveWorkflow, isLoading } = useSaveLibraryWorkflow();
|
||||
return (
|
||||
<IAIIconButton
|
||||
icon={<FaSave />}
|
||||
onClick={saveWorkflow}
|
||||
isLoading={isLoading}
|
||||
tooltip={t('workflows.saveWorkflow')}
|
||||
aria-label={t('workflows.saveWorkflow')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SaveLibraryWorkflowButton);
|
@ -1,17 +1,17 @@
|
||||
import { useDisclosure } from '@chakra-ui/react';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { WorkflowLibraryContext } from 'features/nodes/components/flow/WorkflowLibrary/context';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaFolderOpen } from 'react-icons/fa';
|
||||
import WorkflowLibraryModal from './WorkflowLibraryModal';
|
||||
import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
|
||||
|
||||
const WorkflowLibraryButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const disclosure = useDisclosure();
|
||||
|
||||
return (
|
||||
<WorkflowLibraryContext.Provider value={disclosure}>
|
||||
<WorkflowLibraryModalContext.Provider value={disclosure}>
|
||||
<IAIIconButton
|
||||
icon={<FaFolderOpen />}
|
||||
onClick={disclosure.onOpen}
|
||||
@ -19,7 +19,7 @@ const WorkflowLibraryButton = () => {
|
||||
aria-label={t('workflows.workflowLibrary')}
|
||||
/>
|
||||
<WorkflowLibraryModal />
|
||||
</WorkflowLibraryContext.Provider>
|
||||
</WorkflowLibraryModalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/react';
|
||||
import WorkflowLibraryListWrapper from 'features/workflowLibrary/components/WorkflowLibraryListWrapper';
|
||||
import WorkflowLibrarySystemList from 'features/workflowLibrary/components/WorkflowLibrarySystemList';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import WorkflowLibraryUserList from './WorkflowLibraryUserList';
|
||||
|
||||
const WorkflowLibraryContent = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Tabs w="full" h="full" isLazy>
|
||||
<TabList w="10rem" layerStyle="second" borderRadius="base" p={2}>
|
||||
<Tab>{t('workflows.user')}</Tab>
|
||||
<Tab>{t('workflows.system')}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<WorkflowLibraryListWrapper>
|
||||
<WorkflowLibraryUserList />
|
||||
</WorkflowLibraryListWrapper>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<WorkflowLibraryListWrapper>
|
||||
<WorkflowLibrarySystemList />
|
||||
</WorkflowLibraryListWrapper>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryContent);
|
@ -0,0 +1,88 @@
|
||||
import { Flex, Heading, Spacer, Text } from '@chakra-ui/react';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import dateFormat, { masks } from 'dateformat';
|
||||
import { useDeleteLibraryWorkflow } from 'features/workflowLibrary/hooks/useDeleteLibraryWorkflow';
|
||||
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
|
||||
import { useWorkflowLibraryModalContext } from 'features/workflowLibrary/context/useWorkflowLibraryModalContext';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { WorkflowRecordListItemDTO } from 'services/api/types';
|
||||
|
||||
type Props = {
|
||||
workflowDTO: WorkflowRecordListItemDTO;
|
||||
};
|
||||
|
||||
const WorkflowLibraryListItem = ({ workflowDTO }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { onClose } = useWorkflowLibraryModalContext();
|
||||
const { deleteWorkflow, deleteWorkflowResult } = useDeleteLibraryWorkflow({});
|
||||
const { getAndLoadWorkflow, getAndLoadWorkflowResult } =
|
||||
useGetAndLoadLibraryWorkflow({ onSuccess: onClose });
|
||||
|
||||
const handleDeleteWorkflow = useCallback(() => {
|
||||
deleteWorkflow(workflowDTO.workflow_id);
|
||||
}, [deleteWorkflow, workflowDTO.workflow_id]);
|
||||
|
||||
const handleGetAndLoadWorkflow = useCallback(() => {
|
||||
getAndLoadWorkflow(workflowDTO.workflow_id);
|
||||
}, [getAndLoadWorkflow, workflowDTO.workflow_id]);
|
||||
|
||||
return (
|
||||
<Flex key={workflowDTO.workflow_id} w="full">
|
||||
<Flex w="full" alignItems="center" gap={2}>
|
||||
<Flex flexDir="column" flexGrow={1}>
|
||||
<Flex alignItems="center" w="full">
|
||||
<Heading size="sm">
|
||||
{workflowDTO.name || t('workflows.unnamedWorkflow')}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<Text fontSize="sm" variant="subtext">
|
||||
{t('common.updated')}:{' '}
|
||||
{dateFormat(workflowDTO.updated_at, masks.shortDate)}{' '}
|
||||
{dateFormat(workflowDTO.updated_at, masks.shortTime)}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex alignItems="center" w="full">
|
||||
{workflowDTO.description ? (
|
||||
<Text fontSize="sm" noOfLines={1}>
|
||||
{workflowDTO.description}
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
fontSize="sm"
|
||||
variant="subtext"
|
||||
fontStyle="italic"
|
||||
noOfLines={1}
|
||||
>
|
||||
{t('workflows.noDescription')}
|
||||
</Text>
|
||||
)}
|
||||
<Spacer />
|
||||
<Text fontSize="sm" variant="subtext">
|
||||
{t('common.created')}:{' '}
|
||||
{dateFormat(workflowDTO.created_at, masks.shortDate)}{' '}
|
||||
{dateFormat(workflowDTO.created_at, masks.shortTime)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<IAIButton
|
||||
onClick={handleGetAndLoadWorkflow}
|
||||
isLoading={getAndLoadWorkflowResult.isLoading}
|
||||
aria-label={t('workflows.openWorkflow')}
|
||||
>
|
||||
{t('common.load')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
colorScheme="error"
|
||||
onClick={handleDeleteWorkflow}
|
||||
isLoading={deleteWorkflowResult.isLoading}
|
||||
aria-label={t('workflows.deleteWorkflow')}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</IAIButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryListItem);
|
@ -0,0 +1,21 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
|
||||
const WorkflowLibraryListWrapper = (props: PropsWithChildren) => {
|
||||
return (
|
||||
<Flex
|
||||
w="full"
|
||||
h="full"
|
||||
flexDir="column"
|
||||
layerStyle="second"
|
||||
py={2}
|
||||
px={4}
|
||||
gap={2}
|
||||
borderRadius="base"
|
||||
>
|
||||
{props.children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryListWrapper);
|
@ -7,14 +7,14 @@ import {
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
} from '@chakra-ui/react';
|
||||
import WorkflowLibraryContent from 'features/nodes/components/flow/WorkflowLibrary/WorkflowLibraryContent';
|
||||
import { useWorkflowLibraryContext } from 'features/nodes/components/flow/WorkflowLibrary/useWorkflowLibraryContext';
|
||||
import WorkflowLibraryContent from 'features/workflowLibrary/components/WorkflowLibraryContent';
|
||||
import { useWorkflowLibraryModalContext } from 'features/workflowLibrary/context/useWorkflowLibraryModalContext';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const WorkflowLibraryModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onClose } = useWorkflowLibraryContext();
|
||||
const { isOpen, onClose } = useWorkflowLibraryModalContext();
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} isCentered>
|
||||
<ModalOverlay />
|
@ -52,10 +52,6 @@ const WorkflowLibraryPagination = ({ page, setPage, data }: Props) => {
|
||||
return pages;
|
||||
}, [data.pages, page, setPage]);
|
||||
|
||||
if (data.items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonGroup>
|
||||
<IAIIconButton
|
||||
@ -68,6 +64,7 @@ const WorkflowLibraryPagination = ({ page, setPage, data }: Props) => {
|
||||
{pages.map((p) => (
|
||||
<IAIButton
|
||||
w={10}
|
||||
isDisabled={data.pages === 1}
|
||||
onClick={p.page === page ? undefined : p.onClick}
|
||||
variant={p.page === page ? 'invokeAI' : 'ghost'}
|
||||
key={p.page}
|
@ -0,0 +1,46 @@
|
||||
import { Divider, Flex, Heading, Spacer } from '@chakra-ui/react';
|
||||
import {
|
||||
IAINoContentFallback,
|
||||
IAINoContentFallbackWithSpinner,
|
||||
} from 'common/components/IAIImageFallback';
|
||||
import WorkflowLibraryListItem from 'features/workflowLibrary/components/WorkflowLibraryListItem';
|
||||
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useListRecentWorkflowsQuery } from 'services/api/endpoints/workflows';
|
||||
|
||||
const WorkflowLibraryRecentList = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isError } = useListRecentWorkflowsQuery();
|
||||
|
||||
if (isLoading) {
|
||||
return <IAINoContentFallbackWithSpinner label={t('workflows.loading')} />;
|
||||
}
|
||||
|
||||
if (!data || isError) {
|
||||
return <IAINoContentFallback label={t('workflows.problemLoading')} />;
|
||||
}
|
||||
|
||||
if (!data.items.length) {
|
||||
return <IAINoContentFallback label={t('workflows.noRecentWorkflows')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap={4} alignItems="center" h={10} flexShrink={0} flexGrow={0}>
|
||||
<Heading size="md">{t('workflows.recentWorkflows')}</Heading>
|
||||
<Spacer />
|
||||
</Flex>
|
||||
<Divider />
|
||||
<ScrollableContent>
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
{data.items.map((w) => (
|
||||
<WorkflowLibraryListItem key={w.workflow_id} workflowDTO={w} />
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryRecentList);
|
@ -0,0 +1,117 @@
|
||||
import { Divider, Flex, Heading, Spacer } from '@chakra-ui/react';
|
||||
import { SelectItem } from '@mantine/core';
|
||||
import {
|
||||
IAINoContentFallback,
|
||||
IAINoContentFallbackWithSpinner,
|
||||
} from 'common/components/IAIImageFallback';
|
||||
import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
||||
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||
import WorkflowLibraryListItem from 'features/workflowLibrary/components/WorkflowLibraryListItem';
|
||||
import WorkflowLibraryPagination from 'features/workflowLibrary/components/WorkflowLibraryPagination';
|
||||
import { memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useListSystemWorkflowsQuery } from 'services/api/endpoints/workflows';
|
||||
import { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
|
||||
|
||||
const ORDER_BY_DATA: SelectItem[] = [
|
||||
{ value: 'created_at', label: 'Created' },
|
||||
{ value: 'updated_at', label: 'Updated' },
|
||||
{ value: 'name', label: 'Name' },
|
||||
];
|
||||
|
||||
const DIRECTION_DATA: SelectItem[] = [
|
||||
{ value: 'ASC', label: 'Ascending' },
|
||||
{ value: 'DESC', label: 'Descending' },
|
||||
];
|
||||
|
||||
const WorkflowLibraryList = () => {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(0);
|
||||
const [order_by, setOrderBy] = useState<WorkflowRecordOrderBy>('created_at');
|
||||
const [direction, setDirection] = useState<SQLiteDirection>('ASC');
|
||||
const { data, isLoading, isError, isFetching } =
|
||||
useListSystemWorkflowsQuery();
|
||||
|
||||
const handleChangeOrderBy = useCallback(
|
||||
(value: string | null) => {
|
||||
if (!value || value === order_by) {
|
||||
return;
|
||||
}
|
||||
setOrderBy(value as WorkflowRecordOrderBy);
|
||||
setPage(0);
|
||||
},
|
||||
[order_by]
|
||||
);
|
||||
|
||||
const handleChangeDirection = useCallback(
|
||||
(value: string | null) => {
|
||||
if (!value || value === direction) {
|
||||
return;
|
||||
}
|
||||
setDirection(value as SQLiteDirection);
|
||||
setPage(0);
|
||||
},
|
||||
[direction]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <IAINoContentFallbackWithSpinner label={t('workflows.loading')} />;
|
||||
}
|
||||
|
||||
if (!data || isError) {
|
||||
return <IAINoContentFallback label={t('workflows.problemLoading')} />;
|
||||
}
|
||||
|
||||
if (!data.items.length) {
|
||||
return <IAINoContentFallback label={t('workflows.noSystemWorkflows')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap={4} alignItems="center" h={10} flexShrink={0} flexGrow={0}>
|
||||
<Heading size="md">{t('workflows.systemWorkflows')}</Heading>
|
||||
<Spacer />
|
||||
<IAIMantineSelect
|
||||
label={t('common.orderBy')}
|
||||
value={order_by}
|
||||
data={ORDER_BY_DATA}
|
||||
onChange={handleChangeOrderBy}
|
||||
formControlProps={{
|
||||
w: '12rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
}}
|
||||
disabled={isFetching}
|
||||
/>
|
||||
<IAIMantineSelect
|
||||
label={t('common.direction')}
|
||||
value={direction}
|
||||
data={DIRECTION_DATA}
|
||||
onChange={handleChangeDirection}
|
||||
formControlProps={{
|
||||
w: '12rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
}}
|
||||
disabled={isFetching}
|
||||
/>
|
||||
</Flex>
|
||||
<Divider />
|
||||
<ScrollableContent>
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
{data.items.map((w) => (
|
||||
<WorkflowLibraryListItem key={w.workflow_id} workflowDTO={w} />
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
<Divider />
|
||||
<Flex w="full" justifyContent="space-around">
|
||||
<WorkflowLibraryPagination data={data} page={page} setPage={setPage} />
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryList);
|
@ -0,0 +1,186 @@
|
||||
import { CloseIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Divider,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Spacer,
|
||||
} from '@chakra-ui/react';
|
||||
import { SelectItem } from '@mantine/core';
|
||||
import {
|
||||
IAINoContentFallback,
|
||||
IAINoContentFallbackWithSpinner,
|
||||
} from 'common/components/IAIImageFallback';
|
||||
import IAIMantineSelect from 'common/components/IAIMantineSelect';
|
||||
import ScrollableContent from 'features/nodes/components/sidePanel/ScrollableContent';
|
||||
import WorkflowLibraryListItem from 'features/workflowLibrary/components/WorkflowLibraryListItem';
|
||||
import WorkflowLibraryPagination from 'features/workflowLibrary/components/WorkflowLibraryPagination';
|
||||
import { ChangeEvent, KeyboardEvent, memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useListWorkflowsQuery } from 'services/api/endpoints/workflows';
|
||||
import { SQLiteDirection, WorkflowRecordOrderBy } from 'services/api/types';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
const ORDER_BY_DATA: SelectItem[] = [
|
||||
{ value: 'opened_at', label: 'Recently Opened' },
|
||||
{ value: 'created_at', label: 'Created' },
|
||||
{ value: 'updated_at', label: 'Updated' },
|
||||
{ value: 'name', label: 'Name' },
|
||||
];
|
||||
|
||||
const DIRECTION_DATA: SelectItem[] = [
|
||||
{ value: 'ASC', label: 'Ascending' },
|
||||
{ value: 'DESC', label: 'Descending' },
|
||||
];
|
||||
|
||||
const WorkflowLibraryList = () => {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(0);
|
||||
const [filter_text, setFilterText] = useState('');
|
||||
const [order_by, setOrderBy] = useState<WorkflowRecordOrderBy>('opened_at');
|
||||
const [direction, setDirection] = useState<SQLiteDirection>('ASC');
|
||||
const [debouncedFilterText] = useDebounce(filter_text, 500, {
|
||||
leading: true,
|
||||
});
|
||||
const { data, isLoading, isError, isFetching } = useListWorkflowsQuery({
|
||||
page,
|
||||
per_page: PER_PAGE,
|
||||
order_by,
|
||||
direction,
|
||||
filter_name: debouncedFilterText,
|
||||
});
|
||||
|
||||
const handleChangeOrderBy = useCallback(
|
||||
(value: string | null) => {
|
||||
if (!value || value === order_by) {
|
||||
return;
|
||||
}
|
||||
setOrderBy(value as WorkflowRecordOrderBy);
|
||||
setPage(0);
|
||||
},
|
||||
[order_by]
|
||||
);
|
||||
|
||||
const handleChangeDirection = useCallback(
|
||||
(value: string | null) => {
|
||||
if (!value || value === direction) {
|
||||
return;
|
||||
}
|
||||
setDirection(value as SQLiteDirection);
|
||||
setPage(0);
|
||||
},
|
||||
[direction]
|
||||
);
|
||||
|
||||
const resetFilterText = useCallback(() => {
|
||||
setFilterText('');
|
||||
setPage(0);
|
||||
}, []);
|
||||
|
||||
const handleKeydownFilterText = useCallback(
|
||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||
// exit search mode on escape
|
||||
if (e.key === 'Escape') {
|
||||
resetFilterText();
|
||||
e.preventDefault();
|
||||
setPage(0);
|
||||
}
|
||||
},
|
||||
[resetFilterText]
|
||||
);
|
||||
|
||||
const handleChangeFilterText = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterText(e.target.value);
|
||||
setPage(0);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <IAINoContentFallbackWithSpinner label={t('workflows.loading')} />;
|
||||
}
|
||||
|
||||
if (!data || isError) {
|
||||
return <IAINoContentFallback label={t('workflows.problemLoading')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap={4} alignItems="center" h={10} flexShrink={0} flexGrow={0}>
|
||||
<Heading size="md">{t('workflows.userWorkflows')}</Heading>
|
||||
<Spacer />
|
||||
<InputGroup w="20rem">
|
||||
<Input
|
||||
placeholder={t('workflows.searchWorkflows')}
|
||||
value={filter_text}
|
||||
onKeyDown={handleKeydownFilterText}
|
||||
onChange={handleChangeFilterText}
|
||||
data-testid="workflow-search-input"
|
||||
/>
|
||||
{filter_text.trim().length && (
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
onClick={resetFilterText}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
aria-label={t('workflows.clearWorkflowSearchFilter')}
|
||||
opacity={0.5}
|
||||
icon={<CloseIcon boxSize={2} />}
|
||||
/>
|
||||
</InputRightElement>
|
||||
)}
|
||||
</InputGroup>
|
||||
<IAIMantineSelect
|
||||
label={t('common.orderBy')}
|
||||
value={order_by}
|
||||
data={ORDER_BY_DATA}
|
||||
onChange={handleChangeOrderBy}
|
||||
formControlProps={{
|
||||
w: '15rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
}}
|
||||
disabled={isFetching}
|
||||
/>
|
||||
<IAIMantineSelect
|
||||
label={t('common.direction')}
|
||||
value={direction}
|
||||
data={DIRECTION_DATA}
|
||||
onChange={handleChangeDirection}
|
||||
formControlProps={{
|
||||
w: '12rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
}}
|
||||
disabled={isFetching}
|
||||
/>
|
||||
</Flex>
|
||||
<Divider />
|
||||
{data.items.length ? (
|
||||
<ScrollableContent>
|
||||
<Flex w="full" h="full" gap={2} flexDir="column">
|
||||
{data.items.map((w) => (
|
||||
<WorkflowLibraryListItem key={w.workflow_id} workflowDTO={w} />
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollableContent>
|
||||
) : (
|
||||
<IAINoContentFallback label={t('workflows.noUserWorkflows')} />
|
||||
)}
|
||||
<Divider />
|
||||
<Flex w="full" justifyContent="space-around">
|
||||
<WorkflowLibraryPagination data={data} page={page} setPage={setPage} />
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WorkflowLibraryList);
|
@ -1,6 +1,5 @@
|
||||
import { UseDisclosureReturn } from '@chakra-ui/react';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const WorkflowLibraryContext = createContext<UseDisclosureReturn | null>(
|
||||
null
|
||||
);
|
||||
export const WorkflowLibraryModalContext =
|
||||
createContext<UseDisclosureReturn | null>(null);
|
@ -0,0 +1,12 @@
|
||||
import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useWorkflowLibraryModalContext = () => {
|
||||
const context = useContext(WorkflowLibraryModalContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useWorkflowLibraryContext must be used within a WorkflowLibraryContext.Provider'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import { useAppToaster } from 'app/components/Toaster';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
|
||||
import { zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { omit } from 'lodash-es';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCreateWorkflowMutation } from 'services/api/endpoints/workflows';
|
||||
|
||||
export const useDuplicateLibraryWorkflow = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const workflow = useWorkflow();
|
||||
const [createWorkflow, createWorkflowResult] = useCreateWorkflowMutation();
|
||||
const toaster = useAppToaster();
|
||||
const duplicateWorkflow = useCallback(async () => {
|
||||
try {
|
||||
const data = await createWorkflow(omit(workflow, 'id')).unwrap();
|
||||
const createdWorkflow = zWorkflowV2.parse(data.workflow);
|
||||
dispatch(workflowLoaded(createdWorkflow));
|
||||
toaster({
|
||||
title: t('workflows.workflowSaved'),
|
||||
status: 'success',
|
||||
});
|
||||
} catch (e) {
|
||||
toaster({
|
||||
title: t('workflows.problemSavingWorkflow'),
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
}, [workflow, dispatch, toaster, t, createWorkflow]);
|
||||
return {
|
||||
duplicateWorkflow,
|
||||
isLoading: createWorkflowResult.isLoading,
|
||||
isError: createWorkflowResult.isError,
|
||||
};
|
||||
};
|
@ -4,12 +4,14 @@ import { useWorkflow } from 'features/nodes/hooks/useWorkflow';
|
||||
import { workflowLoaded } from 'features/nodes/store/nodesSlice';
|
||||
import { zWorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
useCreateWorkflowMutation,
|
||||
useUpdateWorkflowMutation,
|
||||
} from 'services/api/endpoints/workflows';
|
||||
|
||||
export const useSaveWorkflow = () => {
|
||||
export const useSaveLibraryWorkflow = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const workflow = useWorkflow();
|
||||
const [updateWorkflow, updateWorkflowResult] = useUpdateWorkflowMutation();
|
||||
@ -18,29 +20,32 @@ export const useSaveWorkflow = () => {
|
||||
const saveWorkflow = useCallback(async () => {
|
||||
try {
|
||||
if (workflow.id) {
|
||||
console.log('update workflow');
|
||||
const data = await updateWorkflow(workflow).unwrap();
|
||||
const updatedWorkflow = zWorkflowV2.parse(data.workflow);
|
||||
dispatch(workflowLoaded(updatedWorkflow));
|
||||
toaster({
|
||||
title: t('workflows.workflowSaved'),
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
console.log('create workflow');
|
||||
const data = await createWorkflow(workflow).unwrap();
|
||||
const createdWorkflow = zWorkflowV2.parse(data.workflow);
|
||||
dispatch(workflowLoaded(createdWorkflow));
|
||||
}
|
||||
toaster({
|
||||
title: 'Workflow saved',
|
||||
title: t('workflows.workflowSaved'),
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
toaster({
|
||||
title: 'Failed to save workflow',
|
||||
// description: e.message,
|
||||
title: t('workflows.problemSavingWorkflow'),
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
}, [workflow, toaster, updateWorkflow, dispatch, createWorkflow]);
|
||||
return saveWorkflow;
|
||||
}, [workflow, updateWorkflow, dispatch, toaster, t, createWorkflow]);
|
||||
return {
|
||||
saveWorkflow,
|
||||
isLoading: updateWorkflowResult.isLoading || createWorkflowResult.isLoading,
|
||||
isError: updateWorkflowResult.isError || createWorkflowResult.isError,
|
||||
};
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
import { WorkflowV2 } from 'features/nodes/types/workflow';
|
||||
import { paths } from 'services/api/schema';
|
||||
import { LIST_TAG, api } from '..';
|
||||
|
||||
@ -12,6 +11,19 @@ export const workflowsApi = api.injectEndpoints({
|
||||
providesTags: (result, error, workflow_id) => [
|
||||
{ type: 'Workflow', id: workflow_id },
|
||||
],
|
||||
onQueryStarted: async (arg, api) => {
|
||||
const { dispatch, queryFulfilled } = api;
|
||||
try {
|
||||
await queryFulfilled;
|
||||
dispatch(
|
||||
workflowsApi.util.invalidateTags([
|
||||
{ type: 'WorkflowsRecent', id: LIST_TAG },
|
||||
])
|
||||
);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
},
|
||||
}),
|
||||
deleteWorkflow: build.mutation<void, string>({
|
||||
query: (workflow_id) => ({
|
||||
@ -21,6 +33,7 @@ export const workflowsApi = api.injectEndpoints({
|
||||
invalidatesTags: (result, error, workflow_id) => [
|
||||
{ type: 'Workflow', id: LIST_TAG },
|
||||
{ type: 'Workflow', id: workflow_id },
|
||||
{ type: 'WorkflowsRecent', id: LIST_TAG },
|
||||
],
|
||||
}),
|
||||
createWorkflow: build.mutation<
|
||||
@ -28,11 +41,14 @@ export const workflowsApi = api.injectEndpoints({
|
||||
paths['/api/v1/workflows/']['post']['requestBody']['content']['application/json']['workflow']
|
||||
>({
|
||||
query: (workflow) => ({
|
||||
url: 'workflows',
|
||||
url: 'workflows/',
|
||||
method: 'POST',
|
||||
body: workflow,
|
||||
body: { workflow },
|
||||
}),
|
||||
invalidatesTags: [{ type: 'Workflow', id: LIST_TAG }],
|
||||
invalidatesTags: [
|
||||
{ type: 'Workflow', id: LIST_TAG },
|
||||
{ type: 'WorkflowsRecent', id: LIST_TAG },
|
||||
],
|
||||
}),
|
||||
updateWorkflow: build.mutation<
|
||||
paths['/api/v1/workflows/i/{workflow_id}']['patch']['responses']['200']['content']['application/json'],
|
||||
@ -41,9 +57,10 @@ export const workflowsApi = api.injectEndpoints({
|
||||
query: (workflow) => ({
|
||||
url: `workflows/i/${workflow.id}`,
|
||||
method: 'PATCH',
|
||||
body: workflow,
|
||||
body: { workflow },
|
||||
}),
|
||||
invalidatesTags: (response, error, workflow) => [
|
||||
{ type: 'WorkflowsRecent', id: LIST_TAG },
|
||||
{ type: 'Workflow', id: LIST_TAG },
|
||||
{ type: 'Workflow', id: workflow.id },
|
||||
],
|
||||
@ -58,13 +75,55 @@ export const workflowsApi = api.injectEndpoints({
|
||||
}),
|
||||
providesTags: [{ type: 'Workflow', id: LIST_TAG }],
|
||||
}),
|
||||
listRecentWorkflows: build.query<
|
||||
paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'],
|
||||
void
|
||||
>({
|
||||
query: () => ({
|
||||
url: 'workflows/',
|
||||
params: {
|
||||
page: 0,
|
||||
per_page: 10,
|
||||
order_by: 'opened_at',
|
||||
direction: 'DESC',
|
||||
},
|
||||
}),
|
||||
providesTags: [{ type: 'WorkflowsRecent', id: LIST_TAG }],
|
||||
}),
|
||||
listSystemWorkflows: build.query<
|
||||
paths['/api/v1/workflows/']['get']['responses']['200']['content']['application/json'],
|
||||
void
|
||||
>({
|
||||
query: () => ({
|
||||
url: 'workflows/',
|
||||
params: {
|
||||
page: 0,
|
||||
per_page: 10,
|
||||
order_by: 'opened_at',
|
||||
direction: 'DESC',
|
||||
},
|
||||
}),
|
||||
transformResponse: () => {
|
||||
return {
|
||||
page: 0,
|
||||
per_page: 10,
|
||||
items: [],
|
||||
total: 0,
|
||||
pages: 0,
|
||||
};
|
||||
},
|
||||
providesTags: [{ type: 'WorkflowsRecent', id: LIST_TAG }],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export const {
|
||||
useGetWorkflowQuery,
|
||||
useLazyGetWorkflowQuery,
|
||||
useCreateWorkflowMutation,
|
||||
useDeleteWorkflowMutation,
|
||||
useUpdateWorkflowMutation,
|
||||
useListWorkflowsQuery,
|
||||
useListRecentWorkflowsQuery,
|
||||
useListSystemWorkflowsQuery,
|
||||
} = workflowsApi;
|
||||
|
@ -41,6 +41,7 @@ export const tagTypes = [
|
||||
'LoRAModel',
|
||||
'SDXLRefinerModel',
|
||||
'Workflow',
|
||||
'WorkflowsRecent',
|
||||
] as const;
|
||||
export type ApiTagDescription = TagDescription<(typeof tagTypes)[number]>;
|
||||
export const LIST_TAG = 'LIST';
|
||||
|
File diff suppressed because one or more lines are too long
@ -114,6 +114,10 @@ export type GraphExecutionState = s['GraphExecutionState'];
|
||||
export type Batch = s['Batch'];
|
||||
export type SessionQueueItemDTO = s['SessionQueueItemDTO'];
|
||||
export type SessionQueueItem = s['SessionQueueItem'];
|
||||
export type WorkflowRecordOrderBy = s['WorkflowRecordOrderBy'];
|
||||
export type SQLiteDirection = s['SQLiteDirection'];
|
||||
export type WorkflowDTO = s['WorkflowRecordDTO'];
|
||||
export type WorkflowRecordListItemDTO = s['WorkflowRecordListItemDTO'];
|
||||
|
||||
// General nodes
|
||||
export type CollectInvocation = s['CollectInvocation'];
|
||||
|
Loading…
Reference in New Issue
Block a user