refactor: add pagination on folders (#4020)
* ⬆️ (uv.lock): Update authlib version from 1.3.2 to 1.3.1 ⬆️ (uv.lock): Update httpx version from 0.27.2 to 0.27.0 ⬆️ (uv.lock): Add new package grpcio-health-checking version 1.62.3 ⬆️ (uv.lock): upgrade weaviate-client package from version 3.26.7 to 4.8.1 and add new dependencies grpcio, grpcio-health-checking, grpcio-tools, httpx, and pydantic. Update package source and distribution URLs accordingly. * ✨ (flows.py): Add pagination support for retrieving a list of flows to improve performance and user experience 📝 (flows.py): Update documentation to reflect changes made for pagination and additional query parameters 📝 (folders.py): Add a new endpoint to retrieve a folder with paginated flows for better organization and readability * ✨ (model.py): add PaginatedFlowResponse class to handle paginated flow responses in API calls * ✨ (test_database.py): add support for fetching all flows by adding "get_all" parameter to the request 🐛 (test_database.py): fix endpoint for fetching read-only starter projects to use correct URL path * packages changes * packages changes * 📝 (paginatorComponent/index.tsx): refactor PaginatorComponent to use constants for default values and improve code readability 🐛 (paginatorComponent/index.tsx): fix issue with setting maxPageIndex when pages prop is provided to PaginatorComponent * 🐛 (storeCardComponent): fix height of store card component to prevent overflow and improve layout aesthetics * ✨ (constants.ts): introduce new constants for search tabs, pagination settings, and store pagination settings to enhance user experience and improve functionality * ✨ (use-post-login-user.ts): add queryClient to UseRequestProcessor to enable refetching queries on mutation settled state 📝 (use-post-login-user.ts): update useLoginUser function to include onSettled callback in options to refetch queries after mutation settles * ✨ (use-get-basic-examples.ts): introduce a new query function to fetch basic examples of flows from the API and update the state with the fetched data. * ✨ (use-get-refresh-flows.ts): introduce new functionality to fetch and process flows with query parameters for components_only, get_all, folder_id, remove_example_flows, page, and size. This allows for more flexible and specific retrieval of flow data. * ✨ (use-get-folder.ts): Add support for pagination and filtering options in the useGetFolderQuery function to enhance flexibility and customization for fetching folder data. * 🔧 (use-get-folders.ts): Remove unused code related to refreshFlows and setStarterProjectId to improve code readability and maintainability ✨ (use-get-folders.ts): Update useGetFoldersQuery to setFolders with the fetched data instead of filtering out starter projects to simplify the logic and improve performance * ✨ (use-upload-flow.ts): update refreshFlows function call to include an object with get_all property set to true to fetch all flows when refreshing * 🔧 (codeAreaModal/index.tsx): remove unused import postCustomComponent to clean up code and improve readability * 📝 (AdminPage/index.tsx): Update initial page size and index values to use constants for better maintainability 📝 (AdminPage/index.tsx): Update resetFilter function to set page size and index using constants for consistency 📝 (AdminPage/index.tsx): Update logic to handle loading state and empty user list based on isIdle state for better user experience 📝 (AdminPage/index.tsx): Update PaginatorComponent props to use constant for rows count and simplify paginate function assignment * ✨ (AppInitPage/index.tsx): introduce new queries to fetch basic examples and folders data for improved functionality and user experience * ✨ (FlowPage/index.tsx): update refreshFlows function call to include a parameter to get all flows at once for better performance. * ✨ (frontend): introduce flowsPagination and setFlowsPagination functions to manage pagination for flows in the UtilityStoreType * ✨ (frontend): introduce new 'folders' and 'setFolders' properties to FoldersStoreType to manage folder data efficiently * ✨ (types.ts): introduce Pagination type to define structure for pagination data in frontend application * ✨ (frontend): introduce PaginatedFlowsType to represent paginated flow data structure in the application. * ✨ (frontend): add optional 'pages' property to PaginatorComponentType to allow customization of the number of pages displayed in the paginator * ✨ (utilityStore.ts): introduce flowsPagination object with default values for page and size to manage pagination in the utility store * ✨ (foldersStore.tsx): introduce new state 'folders' and setter function 'setFolders' to manage folder data in the store * ✨ (ViewPage/index.tsx): update refreshFlows function call to include a parameter to get all flows at once, improving efficiency and reducing unnecessary calls to the backend. * 📝 (StorePage/index.tsx): update constants import to include new pagination constants for better organization ♻️ (StorePage/index.tsx): refactor pagination logic to use new pagination constants for clarity and consistency throughout the file * ✨ (componentsComponent/index.tsx): Add support for pagination feature in ComponentsComponent to improve user experience and performance. * ✨ (myCollectionComponent/index.tsx): introduce pagination and search functionality to improve user experience and data handling in the MyCollectionComponent component * ✨ (Playground/index.tsx): Update refreshFlows function call to include a parameter to get all flows at once for better performance. * ✨ (entities/index.tsx): introduce PaginatedFolderType to represent a folder with pagination information for better organization and handling of paginated data. * ✨ (headerTabsSearchComponent/index.tsx): add support for changing tabs and searching functionality in headerTabsSearchComponent 📝 (headerTabsSearchComponent/index.tsx): update tabsOptions to use constant SEARCH_TABS for consistency and reusability * 📝 (folders.py): Remove redundant import and unused code related to FolderWithPaginatedFlows class 🔧 (folders.py): Update query conditions to use explicit boolean values instead of implicit truthiness for better clarity and readability * 📝 (folders.py): reorganize imports to improve readability and maintain consistency * 📝 (flows.py): import FlowSummary model to support new functionality in the API 📝 (flows.py): add header_flows parameter to read_flows function to return a simplified list of flows if set to true * 📝 (folders.py): remove unnecessary empty line to improve code readability * ✨ (model.py): introduce new FlowSummary class to represent a summary of flow data for better organization and readability * ✨ (pagination_model.py): introduce a new model 'FolderWithPaginatedFlows' to represent a folder with paginated flows for better organization and readability. * 📝 (cardComponent/index.tsx): add missing semicolon to improve code consistency ♻️ (cardComponent/index.tsx): refactor logic to use data parameter directly instead of fetching flow by id to simplify code and improve readability * ✨ (use-get-refresh-flows.ts): introduce new query parameter 'header_flows' to support fetching header flows in API requests. * ✨ (use-get-folders.ts): add functionality to refresh flows when fetching folders to ensure data consistency and up-to-date information. * ✨ (use-upload-flow.ts): add support for fetching header flows along with all flows when refreshing flows in useUploadFlow hook * ✨ (use-redirect-flow-card-click.tsx): introduce a new custom hook 'useFlowCardClick' to handle flow card click events in the newFlowModal component. This hook utilizes react-router-dom for navigation, custom analytics tracking, and various utility functions to manage flow data and update the UI. * ✨ (NewFlowCardComponent/index.tsx): refactor onClick event handler into a separate function handleClick for better readability and maintainability * 📝 (undrawCards/index.tsx): Remove unused imports and variables to clean up the code ♻️ (undrawCards/index.tsx): Refactor onClick event handler to use a separate function for handling flow card click events * ✨ (flowsManager/index.ts): introduce new state and setter for flowToCanvas to manage the flow displayed on canvas * ✨ (flowsManagerStore.ts): introduce new feature to set and update the flow to be displayed on the canvas asynchronously * ✨ (ViewPage/index.tsx): add support for fetching header flows along with all flows when refreshing data to improve data retrieval efficiency * ✨ (collectionCard/index.tsx): introduce useFlowsManagerStore to manage flows in the application 📝 (collectionCard/index.tsx): update handleClick function to set flow to canvas before navigating to editFlowLink * ✨ (Playground/index.tsx): add support for fetching header flows along with all flows when refreshing data in the Playground page. * 🐛 (FlowPage/index.tsx): Fix setCurrentFlow logic to correctly set the current flow based on flowToCanvas value. Add flowToCanvas dependency to useEffect to ensure proper rendering. * ✨ (use-get-flow.ts): introduce a new file to handle API queries for fetching flow data in the frontend application. This file defines a custom hook 'useGetFlow' that makes a GET request to the API endpoint to retrieve flow data based on the provided flow ID. * 📝 (flows.py): update import statement to use FlowHeader instead of FlowSummary for better clarity 📝 (flows.py): update response_model in read_flows endpoint to use FlowHeader for consistency and clarity * ✨ (model.py): rename FlowSummary class to FlowHeader for better clarity and consistency in naming conventions 📝 (model.py): add is_component field to FlowHeader class to indicate if the flow is a component or not * ✨ (use-get-folder.ts): introduce processFlows function to process flows data 📝 (use-get-folder.ts): add cloneDeep function to safely clone data before processing * 🔧 (use-get-refresh-flows.ts): refactor useGetRefreshFlows function to simplify processing of dbDataFlows and update state accordingly * ✨ (FlowPage/index.tsx): Add useGetFlow hook to fetch flow data and update current flow on canvas. Add getFlowToAddToCanvas function to handle fetching and setting flow data on canvas. * ✨ (nodeToolbarComponent/index.tsx): improve user experience by automatically closing the override modal after successful flow override * ✨ (use-post-login-user.ts): add queryClient.refetchQueries for "useGetTags" after successful login to update tags data in the frontend. * ✨ (use-get-flow.ts): add processFlows function to process flow data before returning it to improve code readability and maintainability * 🐛 (use-get-refresh-flows.ts): fix asynchronous flow to correctly handle data retrieval and processing before setting state and returning flows * ✨ (use-get-tags.ts): add functionality to set tags in utility store after fetching them from the API * 📝 (shareModal/index.tsx): Update import statement for useUtilityStore to improve code organization and readability 🔧 (shareModal/index.tsx): Replace useGetTagsQuery with useUtilityStore to manage tags state and remove unnecessary API call for tags data 🔧 (shareModal/index.tsx): Replace references to data with tags variable to ensure consistency and avoid potential bugs 🔧 (shareModal/index.tsx): Update TagsSelector component to use tags variable instead of data for better data management * ✨ (AppInitPage/index.tsx): introduce useGetTagsQuery to fetch tags data from the store API for AppInitPage. * ✨ (StorePage/index.tsx): refactor to use useUtilityStore for tags state management instead of useGetTagsQuery for better separation of concerns and code organization. Update TagsSelector component to use tags from useUtilityStore hook. * ✨ (frontend): introduce Tag type to UtilityStoreType and add tags and setTags functions to manage tags in the store. * ✨ (types.ts): introduce new Tag type to represent a tag with id and name properties * ✨ (utilityStore.ts): introduce new 'tags' array and 'setTags' function to manage tags in the utility store * 📝 (App.css): add a blank line for better readability and consistency in the CSS file * ✨ (test_database.py): add pagination support for reading flows and folders to improve data retrieval efficiency and user experience * ✨ (tabsComponent/index.tsx): refactor changeLocation function to use useCallback hook for better performance and stability ✨ (tabsComponent/index.tsx): update onClick event handler to directly call changeLocation function for cleaner code and improved readability * ✨ (headerTabsSearchComponent/index.tsx): refactor handleChangeTab, handleSearch, handleInputChange, and handleKeyDown functions to use useCallback for better performance and memoization 📝 (headerTabsSearchComponent/index.tsx): update tabActive prop to use the activeTab prop passed from parent component for consistency and clarity * ✨ (myCollectionComponent/index.tsx): refactor filter state initialization to dynamically set based on current location pathname ♻️ (myCollectionComponent/index.tsx): refactor onSearch and onChangeTab functions to use useCallback for better performance and memoization * ✨ (create-query-param-string.ts): introduce a new utility function to build query parameter strings for URLs in the frontend controllers. * ✨ (use-get-folder.ts): introduce buildQueryStringUrl function to create query parameter strings for API requests 📝 (use-get-folder.ts): add comments to explain the purpose of the code and improve code readability 🔧 (use-get-folder.ts): update useGetFolderQuery function to include additional configuration options for the query, such as refetchOnWindowFocus: false * ✨ (use-delete-folders.ts): add functionality to update local store after deleting a folder to keep it in sync with the server data * 📝 (use-post-add-flow.ts): import useFolderStore to access myCollectionId state for refetching queries with the correct folder_id 🐛 (use-post-add-flow.ts): fix queryClient.refetchQueries to include the correct queryKey with folder_id or myCollectionId if response.folder_id is null * ✨ (use-get-refresh-flows.ts): refactor addQueryParams function to use buildQueryStringUrl utility function for better code readability and maintainability * ♻️ (flows.py): remove unnecessary commented out code and add pagination functionality to the router for better code organization and readability * 🔧 (NodeDescription/index.tsx): remove console.log statement for better code cleanliness and readability * ✨ (index.tsx): Add DialogClose component from @radix-ui/react-dialog to handle cancel action in ConfirmationModal. Refactor handleCancel function to improve code readability and maintainability. * ♻️ (use-redirect-flow-card-click.tsx): remove unused setFlowToCanvas function to clean up code and improve maintainability * 📝 (use-patch-update-flow.ts): update onSettled callback to refetch useGetFolders query with the updated folder_id after patching a flow * 🔧 (use-delete-folders.ts): update onSettled function to correctly refetch queries with the specific folder id when deleting folders * ✨ (use-get-folder.ts): update useGetFolderQuery to include additional query parameters for pagination and filtering options * 🔧 (use-post-upload-to-folder.ts): update onSettled callback to refetch useGetFolders query with the correct queryKey and folder_id parameter * ✨ (use-add-flow.ts): add functionality to refresh flows after adding a new flow to ensure the UI is up to date with the latest data. * ✨ (AppInitPage/index.tsx): enable fetching basic examples and tags only when isFetched is true to improve performance and reduce unnecessary API calls * ✨ (myCollectionComponent/index.tsx): refactor onPaginate function to handlePageChange callback for better code organization and readability. Update key prop in ComponentsComponent to include filter and search variables for proper re-rendering. * ✨ (use-get-folder.ts): add placeholderData option to useGetFolderQuery to provide initial data while fetching folder information * [autofix.ci] apply automated fixes * 🐛 (use-post-upload-to-folder.ts): fix queryKey values to correctly refetch queries after uploading a file to a folder * ⬆️ (folders.spec.ts): increase timeout for waiting in test to improve reliability and prevent flakiness * ✨ (folders.spec.ts): update selectors to target specific elements by using the first() method to improve test reliability * ✅ (auto-save-off.spec.ts): update test to match changes in UI for auto-save feature and improve test coverage for hover functionality. * 📝 (model.py): update model fields to set default values for folder_id, is_component, endpoint_name, and description to None for consistency and clarity * ♻️ (index.tsx): refactor isUpdatingFolder logic to include isFetchingFolder variable for better accuracy and readability * 🐛 (index.tsx): fix variable name typo in isLoadingFolder assignment to correctly reference isFetchingFolder * 🐛 (use-patch-update-flow.ts): fix issue with missing closing parenthesis in queryClient.refetchQueries() method 📝 (use-patch-update-flow.ts): update queryKey in queryClient.refetchQueries() to ["useGetFolder"] to correctly refetch the folder data after updating a flow * ✨ (use-save-flow.ts): add support for fetching flow data before saving if not already present to ensure data consistency and accuracy * ✅ (folders.spec.ts): update selectors to target specific elements correctly for testing purposes * ✨ (use-on-file-drop.tsx): add support for checking flow names in a specific collection to prevent duplicates ♻️ (use-add-flow.ts): refactor to use the same logic for checking flow names in a specific collection to prevent duplicates * ✨ (index.tsx): Add useIsMutating hook to check if a folder is being deleted to update UI accordingly ♻️ (index.tsx): Refactor logic to determine if a folder is being updated to include check for folder deletion 🔧 (index.tsx): Refactor Select component to use a separate function for handling value change 🔧 (index.tsx): Refactor Button components to disable based on separate variables for first and last page 🔧 (index.ts): Remove unused import and refactor code to use useRef hook for storing the latest folder id in useGetFolderQuery --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
d0fdc56890
commit
376e4cfdd3
63 changed files with 962 additions and 348 deletions
|
|
@ -12,7 +12,8 @@ import orjson
|
|||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import StreamingResponse
|
||||
from loguru import logger
|
||||
from fastapi_pagination import Page, Params, add_pagination
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
from sqlmodel import Session, and_, col, select
|
||||
|
||||
from langflow.api.utils import cascade_delete_flow, remove_api_keys, validate_is_component
|
||||
|
|
@ -20,6 +21,7 @@ from langflow.api.v1.schemas import FlowListCreate
|
|||
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate
|
||||
from langflow.services.database.models.flow.model import FlowHeader
|
||||
from langflow.services.database.models.flow.utils import get_webhook_component_in_flow
|
||||
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
|
||||
from langflow.services.database.models.folder.model import Folder
|
||||
|
|
@ -121,7 +123,7 @@ def create_flow(
|
|||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.get("/", response_model=list[FlowRead], status_code=200)
|
||||
@router.get("/", response_model=list[FlowRead] | Page[FlowRead] | list[FlowHeader], status_code=200)
|
||||
def read_flows(
|
||||
*,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
|
|
@ -129,56 +131,72 @@ def read_flows(
|
|||
settings_service: SettingsService = Depends(get_settings_service),
|
||||
remove_example_flows: bool = False,
|
||||
components_only: bool = False,
|
||||
get_all: bool = False,
|
||||
folder_id: UUID | None = None,
|
||||
params: Params = Depends(),
|
||||
header_flows: bool = False,
|
||||
):
|
||||
"""
|
||||
Retrieve a list of flows.
|
||||
Retrieve a list of flows with pagination support.
|
||||
|
||||
Args:
|
||||
current_user (User): The current authenticated user.
|
||||
session (Session): The database session.
|
||||
settings_service (SettingsService): The settings service.
|
||||
remove_example_flows (bool, optional): Whether to remove example flows. Defaults to False.
|
||||
components_only (bool, optional): Whether to return only components. Defaults to False.
|
||||
|
||||
get_all (bool, optional): Whether to return all flows without pagination. Defaults to False.
|
||||
folder_id (UUID, optional): The folder ID. Defaults to None.
|
||||
params (Params): Pagination parameters.
|
||||
remove_example_flows (bool, optional): Whether to remove example flows. Defaults to False.
|
||||
Returns:
|
||||
List[Dict]: A list of flows in JSON format.
|
||||
Union[list[FlowRead], Page[FlowRead]]: A list of flows or a paginated response containing the list of flows.
|
||||
"""
|
||||
|
||||
try:
|
||||
auth_settings = settings_service.auth_settings
|
||||
|
||||
default_folder = session.exec(select(Folder).where(Folder.name == DEFAULT_FOLDER_NAME)).first()
|
||||
default_folder_id = default_folder.id if default_folder else None
|
||||
|
||||
starter_folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
||||
starter_folder_id = starter_folder.id if starter_folder else None
|
||||
|
||||
if not folder_id:
|
||||
folder_id = default_folder_id
|
||||
|
||||
if auth_settings.AUTO_LOGIN:
|
||||
stmt = select(Flow).where(
|
||||
(Flow.user_id == None) | (Flow.user_id == current_user.id) # noqa: E711
|
||||
)
|
||||
if components_only:
|
||||
stmt = stmt.where(Flow.is_component == True) # noqa: E712
|
||||
flows = session.exec(stmt).all()
|
||||
|
||||
else:
|
||||
flows = current_user.flows
|
||||
|
||||
flows = validate_is_component(flows)
|
||||
if components_only:
|
||||
flows = [flow for flow in flows if flow.is_component]
|
||||
flow_ids = [flow.id for flow in flows]
|
||||
# with the session get the flows that DO NOT have a user_id
|
||||
folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
||||
|
||||
if not remove_example_flows and not components_only:
|
||||
try:
|
||||
example_flows = folder.flows if folder else []
|
||||
for example_flow in example_flows:
|
||||
if example_flow.id not in flow_ids:
|
||||
flows.append(example_flow)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("Error getting example flows")
|
||||
stmt = select(Flow).where(Flow.user_id == current_user.id)
|
||||
|
||||
if remove_example_flows:
|
||||
flows = [flow for flow in flows if flow.folder_id != folder.id]
|
||||
stmt = stmt.where(Flow.folder_id != starter_folder_id)
|
||||
|
||||
if components_only:
|
||||
stmt = stmt.where(Flow.is_component == True) # noqa: E712
|
||||
|
||||
if not get_all:
|
||||
stmt = stmt.where(Flow.folder_id == folder_id)
|
||||
|
||||
if get_all:
|
||||
flows = session.exec(stmt).all()
|
||||
flows = validate_is_component(flows)
|
||||
if components_only:
|
||||
flows = [flow for flow in flows if flow.is_component]
|
||||
if remove_example_flows and starter_folder_id:
|
||||
flows = [flow for flow in flows if flow.folder_id != starter_folder_id]
|
||||
if header_flows:
|
||||
return [
|
||||
{"id": flow.id, "name": flow.name, "folder_id": flow.folder_id, "is_component": flow.is_component}
|
||||
for flow in flows
|
||||
]
|
||||
return flows
|
||||
return paginate(session, stmt, params=params)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
return [jsonable_encoder(flow) for flow in flows]
|
||||
|
||||
|
||||
@router.get("/{flow_id}", response_model=FlowRead, status_code=200)
|
||||
|
|
@ -401,3 +419,35 @@ async def download_multiple_file(
|
|||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||
)
|
||||
return flows_without_api_keys[0]
|
||||
|
||||
|
||||
@router.get("/basic_examples/", response_model=list[FlowRead], status_code=200)
|
||||
def read_basic_examples(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
"""
|
||||
Retrieve a list of basic example flows.
|
||||
|
||||
Args:
|
||||
session (Session): The database session.
|
||||
|
||||
Returns:
|
||||
list[FlowRead]: A list of basic example flows.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Get the starter folder
|
||||
starter_folder = session.exec(select(Folder).where(Folder.name == STARTER_FOLDER_NAME)).first()
|
||||
|
||||
if not starter_folder:
|
||||
return []
|
||||
|
||||
# Get all flows in the starter folder
|
||||
return session.exec(select(Flow).where(Flow.folder_id == starter_folder.id)).all()
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
add_pagination(router)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import orjson
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Response, UploadFile, status
|
||||
from fastapi_pagination import Params
|
||||
from fastapi_pagination.ext.sqlmodel import paginate
|
||||
from sqlalchemy import or_, update
|
||||
from sqlmodel import Session, select
|
||||
|
||||
|
|
@ -8,6 +10,7 @@ from langflow.api.v1.flows import create_flows
|
|||
from langflow.api.v1.schemas import FlowListCreate, FlowListReadWithFolderName
|
||||
from langflow.helpers.flow import generate_unique_flow_name
|
||||
from langflow.helpers.folders import generate_unique_folder_name
|
||||
from langflow.initial_setup.setup import STARTER_FOLDER_NAME
|
||||
from langflow.services.auth.utils import get_current_active_user
|
||||
from langflow.services.database.models.flow.model import Flow, FlowCreate, FlowRead
|
||||
from langflow.services.database.models.folder.constants import DEFAULT_FOLDER_NAME
|
||||
|
|
@ -15,9 +18,9 @@ from langflow.services.database.models.folder.model import (
|
|||
Folder,
|
||||
FolderCreate,
|
||||
FolderRead,
|
||||
FolderReadWithFlows,
|
||||
FolderUpdate,
|
||||
)
|
||||
from langflow.services.database.models.folder.pagination_model import FolderWithPaginatedFlows
|
||||
from langflow.services.database.models.user.model import User
|
||||
from langflow.services.deps import get_session
|
||||
|
||||
|
|
@ -89,25 +92,41 @@ def read_folders(
|
|||
or_(Folder.user_id == current_user.id, Folder.user_id == None) # noqa: E711
|
||||
)
|
||||
).all()
|
||||
folders = [folder for folder in folders if folder.name != STARTER_FOLDER_NAME]
|
||||
return sorted(folders, key=lambda x: x.name != DEFAULT_FOLDER_NAME)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
||||
|
||||
@router.get("/{folder_id}", response_model=FolderReadWithFlows, status_code=200)
|
||||
@router.get("/{folder_id}", response_model=FolderWithPaginatedFlows, status_code=200)
|
||||
def read_folder(
|
||||
*,
|
||||
session: Session = Depends(get_session),
|
||||
folder_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
params: Params = Depends(),
|
||||
is_component: bool = False,
|
||||
is_flow: bool = False,
|
||||
search: str = "",
|
||||
):
|
||||
try:
|
||||
folder = session.exec(select(Folder).where(Folder.id == folder_id, Folder.user_id == current_user.id)).first()
|
||||
if not folder:
|
||||
raise HTTPException(status_code=404, detail="Folder not found")
|
||||
flows_from_current_user_in_folder = [flow for flow in folder.flows if flow.user_id == current_user.id]
|
||||
folder.flows = flows_from_current_user_in_folder
|
||||
return folder
|
||||
|
||||
stmt = select(Flow).where(Flow.folder_id == folder_id, Flow.user_id == current_user.id)
|
||||
|
||||
if Flow.updated_at is not None:
|
||||
stmt = stmt.order_by(Flow.updated_at.desc()) # type: ignore[attr-defined]
|
||||
if is_component:
|
||||
stmt = stmt.where(Flow.is_component == True) # noqa: E712
|
||||
if is_flow:
|
||||
stmt = stmt.where(Flow.is_component == False) # noqa: E712
|
||||
if search:
|
||||
stmt = stmt.where(Flow.name.like(f"%{search}%")) # type: ignore[attr-defined]
|
||||
paginated_flows = paginate(session, stmt, params=params)
|
||||
|
||||
return FolderWithPaginatedFlows(folder=FolderRead.model_validate(folder), flows=paginated_flows)
|
||||
except Exception as e:
|
||||
if "No result found" in str(e):
|
||||
raise HTTPException(status_code=404, detail="Folder not found") from e
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import emoji
|
|||
from emoji import purely_emoji
|
||||
from fastapi import HTTPException, status
|
||||
from loguru import logger
|
||||
from pydantic import field_serializer, field_validator
|
||||
from pydantic import BaseModel, field_serializer, field_validator
|
||||
from sqlalchemy import Text, UniqueConstraint
|
||||
from sqlmodel import JSON, Column, Field, Relationship, SQLModel
|
||||
|
||||
|
|
@ -190,6 +190,22 @@ class FlowRead(FlowBase):
|
|||
folder_id: UUID | None = Field()
|
||||
|
||||
|
||||
class FlowHeader(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
folder_id: UUID | None = None
|
||||
is_component: bool | None = None
|
||||
endpoint_name: str | None = None
|
||||
description: str | None = None
|
||||
|
||||
|
||||
class PaginatedFlowResponse(BaseModel):
|
||||
flows: list[FlowRead]
|
||||
total: int
|
||||
page_size: int
|
||||
page_index: int
|
||||
|
||||
|
||||
class FlowUpdate(SQLModel):
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
from fastapi_pagination import Page
|
||||
|
||||
from langflow.helpers.base_model import BaseModel
|
||||
from langflow.services.database.models.flow.model import Flow
|
||||
from langflow.services.database.models.folder.model import FolderRead
|
||||
|
||||
|
||||
class FolderWithPaginatedFlows(BaseModel):
|
||||
folder: FolderRead
|
||||
flows: Page[Flow]
|
||||
|
|
@ -154,7 +154,8 @@ dependencies = [
|
|||
"spider-client>=0.0.27",
|
||||
"diskcache>=5.6.3",
|
||||
"clickhouse-connect==0.7.19",
|
||||
"assemblyai>=0.33.0"
|
||||
"assemblyai>=0.33.0",
|
||||
"fastapi-pagination>=0.12.29",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
|
|
|||
|
|
@ -67,8 +67,30 @@ async def test_read_flows(client: TestClient, json_flow: str, active_user, logge
|
|||
assert len(response.json()) > 0
|
||||
|
||||
|
||||
async def test_read_flows_pagination(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
response = await client.get("api/v1/flows/", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["page"] == 1
|
||||
assert response.json()["size"] == 50
|
||||
assert response.json()["pages"] == 0
|
||||
assert response.json()["total"] == 0
|
||||
assert len(response.json()["items"]) == 0
|
||||
|
||||
|
||||
async def test_read_flows_pagination_with_params(client: TestClient, json_flow: str, active_user, logged_in_headers):
|
||||
response = await client.get("api/v1/flows/", headers=logged_in_headers, params={"page": 3, "size": 10})
|
||||
assert response.status_code == 200
|
||||
assert response.json()["page"] == 3
|
||||
assert response.json()["size"] == 10
|
||||
assert response.json()["pages"] == 0
|
||||
assert response.json()["total"] == 0
|
||||
assert len(response.json()["items"]) == 0
|
||||
|
||||
|
||||
async def test_read_flows_components_only(client: TestClient, flow_component: dict, logged_in_headers):
|
||||
response = await client.get("api/v1/flows/", headers=logged_in_headers, params={"components_only": True})
|
||||
response = await client.get(
|
||||
"api/v1/flows/", headers=logged_in_headers, params={"components_only": True, "get_all": True}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
names = [flow["name"] for flow in response.json()]
|
||||
assert any("Chat Input Component" in name for name in names)
|
||||
|
|
@ -267,6 +289,52 @@ async def test_delete_folder_with_flows_with_transaction_and_build(
|
|||
assert response.json() == {"vertex_builds": {}}
|
||||
|
||||
|
||||
async def test_get_flows_from_folder_pagination(client: TestClient, logged_in_headers):
|
||||
# Create a new folder
|
||||
folder_name = f"Test Folder {uuid4()}"
|
||||
folder = FolderCreate(name=folder_name, description="Test folder description", components_list=[], flows_list=[])
|
||||
|
||||
response = await client.post("api/v1/folders/", json=folder.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201, f"Expected status code 201, but got {response.status_code}"
|
||||
|
||||
created_folder = response.json()
|
||||
folder_id = created_folder["id"]
|
||||
|
||||
response = await client.get(f"api/v1/folders/{folder_id}", headers=logged_in_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["folder"]["name"] == folder_name
|
||||
assert response.json()["folder"]["description"] == "Test folder description"
|
||||
assert response.json()["flows"]["page"] == 1
|
||||
assert response.json()["flows"]["size"] == 50
|
||||
assert response.json()["flows"]["pages"] == 0
|
||||
assert response.json()["flows"]["total"] == 0
|
||||
assert len(response.json()["flows"]["items"]) == 0
|
||||
|
||||
|
||||
async def test_get_flows_from_folder_pagination_with_params(client: TestClient, logged_in_headers):
|
||||
# Create a new folder
|
||||
folder_name = f"Test Folder {uuid4()}"
|
||||
folder = FolderCreate(name=folder_name, description="Test folder description", components_list=[], flows_list=[])
|
||||
|
||||
response = await client.post("api/v1/folders/", json=folder.model_dump(), headers=logged_in_headers)
|
||||
assert response.status_code == 201, f"Expected status code 201, but got {response.status_code}"
|
||||
|
||||
created_folder = response.json()
|
||||
folder_id = created_folder["id"]
|
||||
|
||||
response = await client.get(
|
||||
f"api/v1/folders/{folder_id}", headers=logged_in_headers, params={"page": 3, "size": 10}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["folder"]["name"] == folder_name
|
||||
assert response.json()["folder"]["description"] == "Test folder description"
|
||||
assert response.json()["flows"]["page"] == 3
|
||||
assert response.json()["flows"]["size"] == 10
|
||||
assert response.json()["flows"]["pages"] == 0
|
||||
assert response.json()["flows"]["total"] == 0
|
||||
assert len(response.json()["flows"]["items"]) == 0
|
||||
|
||||
|
||||
async def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers):
|
||||
flow = orjson.loads(json_flow)
|
||||
data = flow["data"]
|
||||
|
|
@ -412,7 +480,7 @@ async def test_delete_nonexistent_flow(client: TestClient, active_user, logged_i
|
|||
|
||||
|
||||
async def test_read_only_starter_projects(client: TestClient, active_user, logged_in_headers):
|
||||
response = await client.get("api/v1/flows/", headers=logged_in_headers)
|
||||
response = await client.get("api/v1/flows/basic_examples/", headers=logged_in_headers)
|
||||
starter_projects = load_starter_projects()
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == len(starter_projects)
|
||||
|
|
|
|||
1
src/frontend/package-lock.json
generated
1
src/frontend/package-lock.json
generated
|
|
@ -858,7 +858,6 @@
|
|||
},
|
||||
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
|
||||
"version": "1.3.0",
|
||||
"extraneous": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ body {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.react-flow__resize-control.handle {
|
||||
width: 0.75rem !important;
|
||||
height: 0.75rem !important;
|
||||
|
|
|
|||
|
|
@ -36,10 +36,6 @@ export default function NodeDescription({
|
|||
//timeout to wait for the dom to update
|
||||
setTimeout(() => {
|
||||
if (overflowRef.current) {
|
||||
console.log(
|
||||
overflowRef.current.clientHeight,
|
||||
overflowRef.current.scrollHeight,
|
||||
);
|
||||
if (
|
||||
overflowRef.current.clientHeight < overflowRef.current.scrollHeight
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export default function CollectionCardComponent({
|
|||
const selectedFlowsComponentsCards = useFlowsManagerStore(
|
||||
(state) => state.selectedFlowsComponentsCards,
|
||||
);
|
||||
|
||||
function hasPlayground(flow?: FlowType) {
|
||||
if (!flow) {
|
||||
return false;
|
||||
|
|
@ -60,9 +61,9 @@ export default function CollectionCardComponent({
|
|||
e.stopPropagation();
|
||||
track("Playground Button Clicked", { flowId: data.id });
|
||||
setLoadingPlayground(true);
|
||||
const flow = getFlowById(data.id);
|
||||
if (flow) {
|
||||
if (!hasPlayground(flow)) {
|
||||
|
||||
if (data) {
|
||||
if (!hasPlayground(data)) {
|
||||
setErrorData({
|
||||
title: "Error",
|
||||
list: ["This flow doesn't have a playground."],
|
||||
|
|
@ -70,7 +71,7 @@ export default function CollectionCardComponent({
|
|||
setLoadingPlayground(false);
|
||||
return;
|
||||
}
|
||||
setCurrentFlow(flow);
|
||||
setCurrentFlow(data);
|
||||
setOpenPlayground(true);
|
||||
setLoadingPlayground(false);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { track } from "@/customization/utils/analytics";
|
|||
import { createFileUpload } from "@/helpers/create-file-upload";
|
||||
import { getObjectsFromFilelist } from "@/helpers/get-objects-from-filelist";
|
||||
import useUploadFlow from "@/hooks/flows/use-upload-flow";
|
||||
import { useIsFetching } from "@tanstack/react-query";
|
||||
import { useIsFetching, useIsMutating } from "@tanstack/react-query";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FolderType } from "../../../../pages/MainPage/entities";
|
||||
|
|
@ -230,7 +230,21 @@ const SideBarFoldersButtonsComponent = ({
|
|||
exact: false,
|
||||
});
|
||||
|
||||
const isUpdatingFolder = isFetchingFolders || isPending || loading;
|
||||
const isFetchingFolder = !!useIsFetching({
|
||||
queryKey: ["useGetFolder"],
|
||||
exact: false,
|
||||
});
|
||||
|
||||
const isDeletingFolder = !!useIsMutating({
|
||||
mutationKey: ["useDeleteFolders"],
|
||||
});
|
||||
|
||||
const isUpdatingFolder =
|
||||
isFetchingFolders ||
|
||||
isFetchingFolder ||
|
||||
isPending ||
|
||||
loading ||
|
||||
isDeletingFolder;
|
||||
|
||||
const HeaderButtons = () => (
|
||||
<div className="flex shrink-0 items-center justify-between gap-2">
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ const useFileDrop = (folderId: string) => {
|
|||
(state) => state.setFolderIdDragging,
|
||||
);
|
||||
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
const saveFlow = useSaveFlow();
|
||||
|
|
@ -105,7 +106,14 @@ const useFileDrop = (folderId: string) => {
|
|||
}
|
||||
const updatedFlow = { ...selectedFlow, folder_id: folderId };
|
||||
|
||||
const newName = addVersionToDuplicates(updatedFlow, flows ?? []);
|
||||
const flowsToCheckNames = flows?.filter(
|
||||
(f) => f.folder_id === myCollectionId,
|
||||
);
|
||||
|
||||
const newName = addVersionToDuplicates(
|
||||
updatedFlow,
|
||||
flowsToCheckNames ?? [],
|
||||
);
|
||||
|
||||
updatedFlow.name = newName;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useGetFoldersQuery } from "@/controllers/API/queries/folders/use-get-folders";
|
||||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { useIsFetching } from "@tanstack/react-query";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { FolderType } from "../../pages/MainPage/entities";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
|
@ -19,8 +21,12 @@ export default function FolderSidebarNav({
|
|||
}: SidebarNavProps) {
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
const folders = useFolderStore((state) => state.folders);
|
||||
|
||||
const { data: folders, isPending } = useGetFoldersQuery();
|
||||
const isPending = !!useIsFetching({
|
||||
queryKey: ["useGetFolders"],
|
||||
exact: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<nav className={cn(className)} {...props}>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
import {
|
||||
PAGINATION_PAGE,
|
||||
PAGINATION_ROWS_COUNT,
|
||||
PAGINATION_SIZE,
|
||||
} from "@/constants/constants";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
|
|
@ -11,12 +16,13 @@ import IconComponent from "../genericIconComponent";
|
|||
import { Button } from "../ui/button";
|
||||
|
||||
export default function PaginatorComponent({
|
||||
pageSize = 12,
|
||||
pageIndex = 1,
|
||||
rowsCount = [12, 24, 48, 96],
|
||||
pageSize = PAGINATION_SIZE,
|
||||
pageIndex = PAGINATION_PAGE,
|
||||
rowsCount = PAGINATION_ROWS_COUNT,
|
||||
totalRowsCount = 0,
|
||||
paginate,
|
||||
storeComponent = false,
|
||||
pages,
|
||||
}: PaginatorComponentType) {
|
||||
const [size, setPageSize] = useState(pageSize);
|
||||
const [maxIndex, setMaxPageIndex] = useState(
|
||||
|
|
@ -24,9 +30,18 @@ export default function PaginatorComponent({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
setMaxPageIndex(Math.ceil(totalRowsCount / size));
|
||||
setMaxPageIndex(pages ?? Math.ceil(totalRowsCount / size));
|
||||
}, [totalRowsCount]);
|
||||
|
||||
const disableFirstPage = pageIndex <= 1;
|
||||
const disableLastPage = pageIndex === maxIndex;
|
||||
|
||||
const handleValueChange = (pageSize: string) => {
|
||||
setPageSize(Number(pageSize));
|
||||
setMaxPageIndex(pages ?? Math.ceil(totalRowsCount / Number(pageSize)));
|
||||
paginate(1, Number(pageSize));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between px-2">
|
||||
|
|
@ -41,11 +56,7 @@ export default function PaginatorComponent({
|
|||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium">Rows per page</p>
|
||||
<Select
|
||||
onValueChange={(pageSize: string) => {
|
||||
setPageSize(Number(pageSize));
|
||||
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
|
||||
paginate(Number(pageSize), 1);
|
||||
}}
|
||||
onValueChange={handleValueChange}
|
||||
value={pageSize.toString()}
|
||||
>
|
||||
<SelectTrigger className="w-[100px]">
|
||||
|
|
@ -70,7 +81,7 @@ export default function PaginatorComponent({
|
|||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => {
|
||||
paginate(size, 1);
|
||||
paginate(1, size);
|
||||
}}
|
||||
size={"icon"}
|
||||
>
|
||||
|
|
@ -78,10 +89,10 @@ export default function PaginatorComponent({
|
|||
<IconComponent name="ChevronsLeft" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={pageIndex <= 1}
|
||||
disabled={disableFirstPage}
|
||||
onClick={() => {
|
||||
if (pageIndex > 0) {
|
||||
paginate(size, pageIndex - 1);
|
||||
paginate(pageIndex - 1, size);
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
|
|
@ -92,9 +103,9 @@ export default function PaginatorComponent({
|
|||
<IconComponent name="ChevronLeft" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={pageIndex === maxIndex}
|
||||
disabled={disableLastPage}
|
||||
onClick={() => {
|
||||
paginate(size, pageIndex + 1);
|
||||
paginate(pageIndex + 1, size);
|
||||
}}
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
|
|
@ -104,12 +115,12 @@ export default function PaginatorComponent({
|
|||
<IconComponent name="ChevronRight" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={pageIndex === maxIndex}
|
||||
disabled={disableLastPage}
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
size={"icon"}
|
||||
onClick={() => {
|
||||
paginate(size, maxIndex);
|
||||
paginate(maxIndex, size);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export default function StoreCardComponent({
|
|||
data-testid={`card-${convertTestName(data.name)}`}
|
||||
//TODO check color schema
|
||||
className={cn(
|
||||
"group relative flex h-[11rem] flex-col justify-between overflow-hidden",
|
||||
"group relative flex h-[12rem] flex-col justify-between overflow-hidden",
|
||||
!data.is_component &&
|
||||
"hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#5f5f5f0e]",
|
||||
disabled ? "pointer-events-none opacity-50" : "",
|
||||
|
|
|
|||
|
|
@ -937,3 +937,13 @@ export const SHADOW_COLOR_OPTIONS = {
|
|||
|
||||
export const maxSizeFilesInBytes = 10 * 1024 * 1024; // 10MB in bytes
|
||||
export const MAX_TEXT_LENGTH = 99999;
|
||||
|
||||
export const SEARCH_TABS = ["All", "Flows", "Components"];
|
||||
export const PAGINATION_SIZE = 10;
|
||||
export const PAGINATION_PAGE = 1;
|
||||
|
||||
export const STORE_PAGINATION_SIZE = 12;
|
||||
export const STORE_PAGINATION_PAGE = 1;
|
||||
|
||||
export const PAGINATION_ROWS_COUNT = [10, 20, 50, 100];
|
||||
export const STORE_PAGINATION_ROWS_COUNT = [12, 24, 48, 96];
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { UseRequestProcessor } from "../../services/request-processor";
|
|||
export const useLoginUser: useMutationFunctionType<undefined, LoginType> = (
|
||||
options?,
|
||||
) => {
|
||||
const { mutate } = UseRequestProcessor();
|
||||
const { mutate, queryClient } = UseRequestProcessor();
|
||||
|
||||
async function loginUserFn({ password, username }: LoginType): Promise<any> {
|
||||
const res = await api.post(
|
||||
|
|
@ -28,7 +28,13 @@ export const useLoginUser: useMutationFunctionType<undefined, LoginType> = (
|
|||
const mutation: UseMutationResult<LoginType, any, LoginType> = mutate(
|
||||
["useLoginUser"],
|
||||
loginUserFn,
|
||||
options,
|
||||
{
|
||||
...options,
|
||||
onSettled: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["useGetFolders"] });
|
||||
queryClient.refetchQueries({ queryKey: ["useGetTags"] });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return mutation;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import { useQueryFunctionType } from "@/types/api";
|
||||
import { FlowType } from "@/types/flow";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
import { UseRequestProcessor } from "../../services/request-processor";
|
||||
|
||||
export const useGetBasicExamplesQuery: useQueryFunctionType<
|
||||
undefined,
|
||||
FlowType[]
|
||||
> = (options) => {
|
||||
const { query } = UseRequestProcessor();
|
||||
const setExamples = useFlowsManagerStore((state) => state.setExamples);
|
||||
|
||||
const getBasicExamplesFn = async () => {
|
||||
return await api.get<FlowType[]>(`${getURL("FLOWS")}/basic_examples/`);
|
||||
};
|
||||
|
||||
const responseFn = async () => {
|
||||
const { data } = await getBasicExamplesFn();
|
||||
if (data) {
|
||||
setExamples(data);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const queryResult = query(["useGetBasicExamplesQuery"], responseFn, {
|
||||
...options,
|
||||
});
|
||||
|
||||
return queryResult;
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useMutationFunctionType } from "@/types/api";
|
||||
import { FlowType } from "@/types/flow";
|
||||
import { processFlows } from "@/utils/reactflowUtils";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
import { UseRequestProcessor } from "../../services/request-processor";
|
||||
|
||||
interface IGetFlow {
|
||||
id: string;
|
||||
}
|
||||
|
||||
// add types for error handling and success
|
||||
export const useGetFlow: useMutationFunctionType<undefined, IGetFlow> = (
|
||||
options,
|
||||
) => {
|
||||
const { mutate } = UseRequestProcessor();
|
||||
|
||||
const getFlowFn = async (payload: IGetFlow): Promise<FlowType> => {
|
||||
const response = await api.get<FlowType>(
|
||||
`${getURL("FLOWS")}/${payload.id}`,
|
||||
);
|
||||
|
||||
const flowsArrayToProcess = [response.data];
|
||||
const { flows } = processFlows(flowsArrayToProcess);
|
||||
return flows[0];
|
||||
};
|
||||
|
||||
const mutation = mutate(["useGetFlow"], getFlowFn, options);
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
|
@ -1,67 +1,92 @@
|
|||
import buildQueryStringUrl from "@/controllers/utils/create-query-param-string";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { useTypesStore } from "@/stores/typesStore";
|
||||
import { useMutationFunctionType } from "@/types/api";
|
||||
import { FlowType } from "@/types/flow";
|
||||
import { FlowType, PaginatedFlowsType } from "@/types/flow";
|
||||
import {
|
||||
extractFieldsFromComponenents,
|
||||
processFlows,
|
||||
} from "@/utils/reactflowUtils";
|
||||
import { UseMutationResult } from "@tanstack/react-query";
|
||||
import { UseMutationOptions } from "@tanstack/react-query";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
import { UseRequestProcessor } from "../../services/request-processor";
|
||||
|
||||
interface GetFlowsParams {
|
||||
components_only?: boolean;
|
||||
get_all?: boolean;
|
||||
header_flows?: boolean;
|
||||
folder_id?: string;
|
||||
remove_example_flows?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const addQueryParams = (url: string, params: GetFlowsParams): string => {
|
||||
return buildQueryStringUrl(url, params);
|
||||
};
|
||||
|
||||
export const useGetRefreshFlows: useMutationFunctionType<
|
||||
undefined,
|
||||
undefined
|
||||
> = (options?) => {
|
||||
GetFlowsParams
|
||||
> = (options) => {
|
||||
const { mutate } = UseRequestProcessor();
|
||||
|
||||
const setExamples = useFlowsManagerStore((state) => state.setExamples);
|
||||
const setFlows = useFlowsManagerStore((state) => state.setFlows);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const starterProjectId = useFolderStore((state) => state.starterProjectId);
|
||||
|
||||
const getRefreshFlowsFn = async (): Promise<void> => {
|
||||
const getRefreshFlowsFn = async (
|
||||
params: GetFlowsParams,
|
||||
): Promise<FlowType[] | PaginatedFlowsType> => {
|
||||
const url = addQueryParams(`${getURL("FLOWS")}/`, params);
|
||||
const { data } = await api.get<FlowType[]>(url);
|
||||
return data;
|
||||
};
|
||||
|
||||
const mutationFn = async (
|
||||
params?: GetFlowsParams,
|
||||
): Promise<FlowType[] | PaginatedFlowsType> => {
|
||||
try {
|
||||
await getRefreshFlowsFn(params!).then(async (dbDataFlows) => {
|
||||
const dbDataComponents = await getRefreshFlowsFn({
|
||||
components_only: true,
|
||||
get_all: true,
|
||||
});
|
||||
|
||||
if (dbDataComponents) {
|
||||
const { data } = processFlows(dbDataComponents as FlowType[]);
|
||||
useTypesStore.setState((state) => ({
|
||||
data: { ...state.data, ["saved_components"]: data },
|
||||
ComponentFields: extractFieldsFromComponenents({
|
||||
...state.data,
|
||||
["saved_components"]: data,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
if (dbDataFlows) {
|
||||
const flows = Array.isArray(dbDataFlows)
|
||||
? dbDataFlows
|
||||
: dbDataFlows.items;
|
||||
|
||||
setFlows(flows);
|
||||
return flows;
|
||||
}
|
||||
});
|
||||
|
||||
return [];
|
||||
} catch (e) {
|
||||
setErrorData({
|
||||
title: "Could not load flows from database",
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
const response = await api.get<FlowType[]>(`${getURL("FLOWS")}/`);
|
||||
const dbData = response.data;
|
||||
if (dbData) {
|
||||
const { data, flows } = processFlows(dbData);
|
||||
const examples = flows.filter(
|
||||
(flow) => flow.folder_id === starterProjectId,
|
||||
);
|
||||
setExamples(examples);
|
||||
|
||||
const flowsWithoutStarterFolder = flows.filter(
|
||||
(flow) => flow.folder_id !== starterProjectId,
|
||||
);
|
||||
|
||||
setFlows(flowsWithoutStarterFolder);
|
||||
useTypesStore.setState((state) => ({
|
||||
data: { ...state.data, ["saved_components"]: data },
|
||||
ComponentFields: extractFieldsFromComponenents({
|
||||
...state.data,
|
||||
["saved_components"]: data,
|
||||
}),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const mutation: UseMutationResult<void, any, undefined> = mutate(
|
||||
["useGetRefreshFlows"],
|
||||
getRefreshFlowsFn,
|
||||
options,
|
||||
);
|
||||
const mutation = mutate(["useGetRefreshFlows"], mutationFn, {
|
||||
...(options as UseMutationOptions<any, any, void, unknown>),
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,8 +34,13 @@ export const usePatchUpdateFlow: useMutationFunctionType<
|
|||
|
||||
const mutation: UseMutationResult<IPatchUpdateFlow, any, IPatchUpdateFlow> =
|
||||
mutate(["usePatchUpdateFlow"], PatchUpdateFlowFn, {
|
||||
onSettled: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["useGetFolder"] });
|
||||
onSettled: (res) => {
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["useGetFolders", res.folder_id],
|
||||
}),
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["useGetFolder"],
|
||||
});
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { useMutationFunctionType } from "@/types/api";
|
||||
import { UseMutationResult } from "@tanstack/react-query";
|
||||
import { ReactFlowJsonObject } from "reactflow";
|
||||
|
|
@ -19,6 +20,7 @@ export const usePostAddFlow: useMutationFunctionType<
|
|||
IPostAddFlow
|
||||
> = (options?) => {
|
||||
const { mutate, queryClient } = UseRequestProcessor();
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
|
||||
const postAddFlowFn = async (payload: IPostAddFlow): Promise<any> => {
|
||||
const response = await api.post(`${getURL("FLOWS")}/`, {
|
||||
|
|
@ -38,8 +40,10 @@ export const usePostAddFlow: useMutationFunctionType<
|
|||
postAddFlowFn,
|
||||
{
|
||||
...options,
|
||||
onSettled: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["useGetFolder"] });
|
||||
onSettled: (response) => {
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["useGetFolder", response.folder_id ?? myCollectionId],
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { useMutationFunctionType } from "@/types/api";
|
||||
import { UseMutationResult } from "@tanstack/react-query";
|
||||
import { api } from "../../api";
|
||||
|
|
@ -13,12 +14,14 @@ export const useDeleteFolders: useMutationFunctionType<
|
|||
DeleteFoldersParams
|
||||
> = (options?) => {
|
||||
const { mutate, queryClient } = UseRequestProcessor();
|
||||
const setFolders = useFolderStore((state) => state.setFolders);
|
||||
const folders = useFolderStore((state) => state.folders);
|
||||
|
||||
const deleteFolder = async ({
|
||||
folder_id,
|
||||
}: DeleteFoldersParams): Promise<any> => {
|
||||
const res = await api.delete(`${getURL("FOLDERS")}/${folder_id}`);
|
||||
// returning id to use it in onSuccess and delete the folder from the cache
|
||||
await api.delete(`${getURL("FOLDERS")}/${folder_id}`);
|
||||
setFolders(folders.filter((f) => f.id !== folder_id));
|
||||
return folder_id;
|
||||
};
|
||||
|
||||
|
|
@ -28,14 +31,8 @@ export const useDeleteFolders: useMutationFunctionType<
|
|||
DeleteFoldersParams
|
||||
> = mutate(["useDeleteFolders"], deleteFolder, {
|
||||
...options,
|
||||
onSettled: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["useGetFolders"] });
|
||||
},
|
||||
onSuccess: (id) => {
|
||||
queryClient.removeQueries({
|
||||
queryKey: ["useGetFolder", { id }],
|
||||
exact: true,
|
||||
});
|
||||
onSettled: (id) => {
|
||||
queryClient.refetchQueries({ queryKey: ["useGetFolders", id] });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,30 +1,81 @@
|
|||
import { FolderType } from "@/pages/MainPage/entities";
|
||||
import buildQueryStringUrl from "@/controllers/utils/create-query-param-string";
|
||||
import { PaginatedFolderType } from "@/pages/MainPage/entities";
|
||||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { useQueryFunctionType } from "@/types/api";
|
||||
import { processFlows } from "@/utils/reactflowUtils";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useRef } from "react";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
import { UseRequestProcessor } from "../../services/request-processor";
|
||||
|
||||
interface IGetFolder {
|
||||
id: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
is_component?: boolean;
|
||||
is_flow?: boolean;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export const useGetFolderQuery: useQueryFunctionType<IGetFolder, FolderType> = (
|
||||
params,
|
||||
options,
|
||||
) => {
|
||||
const addQueryParams = (url: string, params: IGetFolder): string => {
|
||||
return buildQueryStringUrl(url, params);
|
||||
};
|
||||
|
||||
export const useGetFolderQuery: useQueryFunctionType<
|
||||
IGetFolder,
|
||||
PaginatedFolderType | undefined
|
||||
> = (params, options) => {
|
||||
const { query } = UseRequestProcessor();
|
||||
|
||||
const getFolderFn = async (): Promise<FolderType> => {
|
||||
const res = await api.get(`${getURL("FOLDERS")}/${params.id}`);
|
||||
const data = res.data;
|
||||
const folders = useFolderStore((state) => state.folders);
|
||||
const latestIdRef = useRef("");
|
||||
|
||||
return data;
|
||||
const getFolderFn = async (
|
||||
params: IGetFolder,
|
||||
): Promise<PaginatedFolderType | undefined> => {
|
||||
if (params.id) {
|
||||
if (latestIdRef.current !== params.id) {
|
||||
params.page = 1;
|
||||
}
|
||||
latestIdRef.current = params.id;
|
||||
|
||||
const existingFolder = folders.find((f) => f.id === params.id);
|
||||
if (!existingFolder) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const url = addQueryParams(`${getURL("FOLDERS")}/${params.id}`, params);
|
||||
const { data } = await api.get<PaginatedFolderType>(url);
|
||||
|
||||
const { flows } = processFlows(data.flows.items);
|
||||
|
||||
const dataProcessed = cloneDeep(data);
|
||||
dataProcessed.flows.items = flows;
|
||||
|
||||
return dataProcessed;
|
||||
};
|
||||
|
||||
const queryResult = query(
|
||||
["useGetFolder", { id: params.id }],
|
||||
getFolderFn,
|
||||
options,
|
||||
[
|
||||
"useGetFolder",
|
||||
params.id,
|
||||
{
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
is_component: params.is_component,
|
||||
is_flow: params.is_flow,
|
||||
search: params.search,
|
||||
},
|
||||
],
|
||||
() => getFolderFn(params),
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: true,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
|
||||
return queryResult;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,34 +15,22 @@ export const useGetFoldersQuery: useQueryFunctionType<
|
|||
const { query } = UseRequestProcessor();
|
||||
const { mutateAsync: refreshFlows } = useGetRefreshFlows();
|
||||
|
||||
const setStarterProjectId = useFolderStore(
|
||||
(state) => state.setStarterProjectId,
|
||||
);
|
||||
const setMyCollectionId = useFolderStore((state) => state.setMyCollectionId);
|
||||
const setFolders = useFolderStore((state) => state.setFolders);
|
||||
|
||||
const getFoldersFn = async (): Promise<FolderType[]> => {
|
||||
const res = await api.get(`${getURL("FOLDERS")}/`);
|
||||
const data = res.data;
|
||||
|
||||
const foldersWithoutStarterProjects = data?.filter(
|
||||
(folder) => folder.name !== STARTER_FOLDER_NAME,
|
||||
);
|
||||
|
||||
const starterProjects = data?.find(
|
||||
(folder) => folder.name === STARTER_FOLDER_NAME,
|
||||
);
|
||||
|
||||
setStarterProjectId(starterProjects?.id ?? "");
|
||||
|
||||
const myCollectionId = data?.find((f) => f.name === DEFAULT_FOLDER)?.id;
|
||||
setMyCollectionId(myCollectionId);
|
||||
|
||||
setFolders(data);
|
||||
const { getTypes, types } = useTypesStore.getState();
|
||||
|
||||
await refreshFlows(undefined);
|
||||
await refreshFlows({ get_all: true, header_flows: true });
|
||||
if (!types || Object.keys(types).length === 0) await getTypes();
|
||||
|
||||
return foldersWithoutStarterProjects;
|
||||
return data;
|
||||
};
|
||||
|
||||
const queryResult = query(["useGetFolders"], getFoldersFn, options);
|
||||
|
|
|
|||
|
|
@ -26,8 +26,13 @@ export const usePostUploadFlowToFolder: useMutationFunctionType<
|
|||
|
||||
const mutation = mutate(["usePostUploadFlowToFolder"], uploadFlowToFolderFn, {
|
||||
...options,
|
||||
onSettled: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["useGetFolder"] });
|
||||
onSettled: (res) => {
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["useGetFolders"],
|
||||
});
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ["useGetFolder"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useUtilityStore } from "@/stores/utilityStore";
|
||||
import { useQueryFunctionType } from "@/types/api";
|
||||
import { api } from "../../api";
|
||||
import { getURL } from "../../helpers/constants";
|
||||
|
|
@ -15,6 +16,7 @@ export const useGetTagsQuery: useQueryFunctionType<
|
|||
tagsQueryResponse
|
||||
> = (options) => {
|
||||
const { query } = UseRequestProcessor();
|
||||
const setTags = useUtilityStore((state) => state.setTags);
|
||||
|
||||
const getTagsFn = async () => {
|
||||
return await api.get<tagsQueryResponse>(`${getURL("STORE")}/tags`);
|
||||
|
|
@ -22,6 +24,7 @@ export const useGetTagsQuery: useQueryFunctionType<
|
|||
|
||||
const responseFn = async () => {
|
||||
const { data } = await getTagsFn();
|
||||
setTags(data);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
interface QueryParams {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const buildQueryStringUrl = (baseUrl: string, params: QueryParams): string => {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined) {
|
||||
if (typeof value === "boolean") {
|
||||
queryParams.append(key, value ? "true" : "false");
|
||||
} else {
|
||||
queryParams.append(key, value.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const queryString = queryParams.toString();
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
};
|
||||
|
||||
export default buildQueryStringUrl;
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { useGetRefreshFlows } from "@/controllers/API/queries/flows/use-get-refresh-flows";
|
||||
import { usePostAddFlow } from "@/controllers/API/queries/flows/use-post-add-flow";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
|
|
@ -28,13 +29,20 @@ const useAddFlow = () => {
|
|||
const setFlows = useFlowsManagerStore((state) => state.setFlows);
|
||||
const { deleteFlow } = useDeleteFlow();
|
||||
|
||||
const { setFlowToCanvas } = useFlowsManagerStore();
|
||||
|
||||
const { folderId } = useParams();
|
||||
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
|
||||
const { mutate: postAddFlow } = usePostAddFlow();
|
||||
const { mutate: refreshFlows } = useGetRefreshFlows();
|
||||
|
||||
const addFlow = async (params?: { flow?: FlowType; override?: boolean }) => {
|
||||
const addFlow = async (params?: {
|
||||
flow?: FlowType;
|
||||
override?: boolean;
|
||||
new_blank?: boolean;
|
||||
}) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const flow = cloneDeep(params?.flow) ?? undefined;
|
||||
let flowData = flow
|
||||
|
|
@ -57,9 +65,14 @@ const useAddFlow = () => {
|
|||
await deleteFlow({ id: flowId.id });
|
||||
}
|
||||
}
|
||||
|
||||
const flowsToCheckNames = flows?.filter(
|
||||
(f) => f.folder_id === myCollectionId,
|
||||
);
|
||||
|
||||
const newFlow = createNewFlow(flowData!, folder_id, flow);
|
||||
|
||||
const newName = addVersionToDuplicates(newFlow, flows ?? []);
|
||||
const newName = addVersionToDuplicates(newFlow, flowsToCheckNames ?? []);
|
||||
newFlow.name = newName;
|
||||
newFlow.folder_id = folder_id;
|
||||
|
||||
|
|
@ -78,17 +91,24 @@ const useAddFlow = () => {
|
|||
["saved_components"]: data,
|
||||
}),
|
||||
}));
|
||||
|
||||
refreshFlows({
|
||||
get_all: true,
|
||||
header_flows: true,
|
||||
});
|
||||
|
||||
setFlowToCanvas(createdFlow);
|
||||
resolve(createdFlow.id);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error.response?.data?.detail) {
|
||||
useAlertStore.getState().setErrorData({
|
||||
title: "Could not load flows from database",
|
||||
title: "Could not create flow",
|
||||
list: [error.response?.data?.detail],
|
||||
});
|
||||
} else {
|
||||
useAlertStore.getState().setErrorData({
|
||||
title: "Could not load flows from database",
|
||||
title: "Could not create flow",
|
||||
list: [
|
||||
error.message ??
|
||||
"An unexpected error occurred, please try again",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { useGetFlow } from "@/controllers/API/queries/flows/use-get-flow";
|
||||
import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-update-flow";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { FlowType } from "@/types/flow";
|
||||
import { customStringify } from "@/utils/reactflowUtils";
|
||||
import { ReactFlowJsonObject } from "reactflow";
|
||||
|
||||
const useSaveFlow = () => {
|
||||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
|
|
@ -16,11 +18,12 @@ const useSaveFlow = () => {
|
|||
const setCurrentFlow = useFlowStore((state) => state.setCurrentFlow);
|
||||
const currentSavedFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
|
||||
const { mutate: getFlow } = useGetFlow();
|
||||
const { mutate } = usePatchUpdateFlow();
|
||||
|
||||
const currentFlow = useFlowStore((state) => state.currentFlow);
|
||||
const flowData = currentFlow?.data;
|
||||
|
||||
const { mutate } = usePatchUpdateFlow();
|
||||
|
||||
const saveFlow = async (flow?: FlowType): Promise<void> => {
|
||||
if (
|
||||
customStringify(flow || currentFlow) !== customStringify(currentSavedFlow)
|
||||
|
|
@ -42,12 +45,24 @@ const useSaveFlow = () => {
|
|||
},
|
||||
};
|
||||
}
|
||||
if (flow && flow.data) {
|
||||
|
||||
if (flow) {
|
||||
if (!flow?.data) {
|
||||
getFlow(
|
||||
{ id: flow!.id },
|
||||
{
|
||||
onSuccess: (flowResponse) => {
|
||||
flow!.data = flowResponse.data as ReactFlowJsonObject;
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const { id, name, data, description, folder_id, endpoint_name } =
|
||||
flow;
|
||||
if (!currentSavedFlow?.data?.nodes.length || data.nodes.length > 0) {
|
||||
if (!currentSavedFlow?.data?.nodes.length || data!.nodes.length > 0) {
|
||||
mutate(
|
||||
{ id, name, data, description, folder_id, endpoint_name },
|
||||
{ id, name, data: data!, description, folder_id, endpoint_name },
|
||||
{
|
||||
onSuccess: (updatedFlow) => {
|
||||
setSaveLoading(false);
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ const useUploadFlow = () => {
|
|||
throw new Error("Invalid flow data");
|
||||
}
|
||||
}
|
||||
refreshFlows(undefined);
|
||||
await refreshFlows({ get_all: true, header_flows: true });
|
||||
}
|
||||
} catch (e) {
|
||||
throw e;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import {
|
|||
CODE_PROMPT_DIALOG_SUBTITLE,
|
||||
EDIT_CODE_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { postCustomComponent } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { useDarkStore } from "../../stores/darkStore";
|
||||
import { CodeErrorDataTypeAPI } from "../../types/api";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import GenericIconComponent from "@/components/genericIconComponent";
|
||||
import { DialogClose } from "@radix-ui/react-dialog";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
|
|
@ -70,6 +71,12 @@ function ConfirmationModal({
|
|||
const shouldShowCancel = cancelText;
|
||||
const shouldShowFooter = shouldShowConfirm || shouldShowCancel;
|
||||
|
||||
const handleCancel = () => {
|
||||
setFlag(true);
|
||||
setModalOpen(false);
|
||||
onCancel?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal {...props} open={open} setOpen={setModalOpen}>
|
||||
<BaseModal.Trigger>{triggerChild}</BaseModal.Trigger>
|
||||
|
|
@ -111,17 +118,15 @@ function ConfirmationModal({
|
|||
</Button>
|
||||
)}
|
||||
{shouldShowCancel && (
|
||||
<Button
|
||||
className=""
|
||||
variant={destructiveCancel ? "destructive" : "outline"}
|
||||
onClick={() => {
|
||||
setFlag(true);
|
||||
setModalOpen(false);
|
||||
onCancel?.();
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<DialogClose>
|
||||
<Button
|
||||
className=""
|
||||
variant={destructiveCancel ? "destructive" : "outline"}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
)}
|
||||
</BaseModal.Footer>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -14,14 +14,16 @@ export default function NewFlowCardComponent() {
|
|||
const navigate = useCustomNavigate();
|
||||
const { folderId } = useParams();
|
||||
|
||||
const handleClick = () => {
|
||||
addFlow({ new_blank: true }).then((id) => {
|
||||
navigate(`/flow/${id}${folderId ? `/folder/${folderId}` : ""}`);
|
||||
});
|
||||
track("New Flow Created", { template: "Blank Flow" });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={() => {
|
||||
addFlow().then((id) => {
|
||||
navigate(`/flow/${id}${folderId ? `/folder/${folderId}` : ""}`);
|
||||
});
|
||||
track("New Flow Created", { template: "Blank Flow" });
|
||||
}}
|
||||
onClick={handleClick}
|
||||
className="h-64 w-80 cursor-pointer bg-background pt-4"
|
||||
data-testid="blank-flow"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { track } from "../../../../customization/utils/analytics";
|
||||
import useAddFlow from "../../../../hooks/flows/use-add-flow";
|
||||
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||
import { FlowType } from "../../../../types/flow";
|
||||
import { updateIds } from "../../../../utils/reactflowUtils";
|
||||
|
||||
export function useFlowCardClick() {
|
||||
const navigate = useNavigate();
|
||||
const addFlow = useAddFlow();
|
||||
|
||||
const handleFlowCardClick = async (flow: FlowType, folderIdUrl: string) => {
|
||||
try {
|
||||
updateIds(flow.data!);
|
||||
const id = await addFlow({ flow });
|
||||
navigate(`/flow/${id}/folder/${folderIdUrl}`);
|
||||
track("New Flow Created", { template: `${flow.name} Template` });
|
||||
} catch (error) {
|
||||
console.error("Error handling flow card click:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return handleFlowCardClick;
|
||||
}
|
||||
|
|
@ -10,9 +10,6 @@ import APIRequest from "../../../../assets/undraw_real_time_analytics_re_yliv.sv
|
|||
import BasicPrompt from "../../../../assets/undraw_short_bio_re_fmx0.svg?react";
|
||||
import TransferFiles from "../../../../assets/undraw_transfer_files_re_a2a9.svg?react";
|
||||
|
||||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import { track } from "@/customization/utils/analytics";
|
||||
import useAddFlow from "@/hooks/flows/use-add-flow";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
|
@ -22,17 +19,17 @@ import {
|
|||
import { useFolderStore } from "../../../../stores/foldersStore";
|
||||
import { UndrawCardComponentProps } from "../../../../types/components";
|
||||
import { updateIds } from "../../../../utils/reactflowUtils";
|
||||
import { useFlowCardClick } from "../hooks/use-redirect-flow-card-click";
|
||||
|
||||
export default function UndrawCardComponent({
|
||||
flow,
|
||||
}: UndrawCardComponentProps): JSX.Element {
|
||||
const addFlow = useAddFlow();
|
||||
const navigate = useCustomNavigate();
|
||||
const { folderId } = useParams();
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
|
||||
const folderIdUrl = folderId ?? myCollectionId;
|
||||
|
||||
const handleFlowCardClick = useFlowCardClick();
|
||||
|
||||
function selectImage() {
|
||||
switch (flow.name) {
|
||||
case "Blog Writer":
|
||||
|
|
@ -140,13 +137,7 @@ export default function UndrawCardComponent({
|
|||
|
||||
return (
|
||||
<Card
|
||||
onClick={() => {
|
||||
updateIds(flow.data!);
|
||||
addFlow({ flow }).then((id) => {
|
||||
navigate(`/flow/${id}/folder/${folderIdUrl}`);
|
||||
});
|
||||
track("New Flow Created", { template: `${flow.name} Template` });
|
||||
}}
|
||||
onClick={() => handleFlowCardClick(flow, folderIdUrl!)}
|
||||
className="h-64 w-80 cursor-pointer bg-background pt-4"
|
||||
>
|
||||
<CardContent className="h-full w-full">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useGetTagsQuery } from "@/controllers/API/queries/store";
|
||||
import useSaveFlow from "@/hooks/flows/use-save-flow";
|
||||
import { useUtilityStore } from "@/stores/utilityStore";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import EditFlowSettings from "../../components/editFlowSettingsComponent";
|
||||
|
|
@ -59,7 +59,7 @@ export default function ShareModal({
|
|||
{ id: string; name: string }[]
|
||||
>([]);
|
||||
const saveFlow = useSaveFlow();
|
||||
const { data, isFetching } = useGetTagsQuery();
|
||||
const tags = useUtilityStore((state) => state.tags);
|
||||
|
||||
const [loadingNames, setLoadingNames] = useState(false);
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ export default function ShareModal({
|
|||
if (!update)
|
||||
saveFlowStore(
|
||||
flow!,
|
||||
getTagsIds(selectedTags, cloneDeep(data) ?? []),
|
||||
getTagsIds(selectedTags, cloneDeep(tags) ?? []),
|
||||
sharePublic,
|
||||
).then(successShare, (err) => {
|
||||
setErrorData({
|
||||
|
|
@ -125,7 +125,7 @@ export default function ShareModal({
|
|||
else
|
||||
updateFlowStore(
|
||||
flow!,
|
||||
getTagsIds(selectedTags, cloneDeep(data) ?? []),
|
||||
getTagsIds(selectedTags, cloneDeep(tags) ?? []),
|
||||
sharePublic,
|
||||
unavaliableNames.find((e) => e.name === name)!.id,
|
||||
).then(successShare, (err) => {
|
||||
|
|
@ -226,8 +226,8 @@ export default function ShareModal({
|
|||
</div>
|
||||
<div className="mt-3 flex h-8 w-full">
|
||||
<TagsSelector
|
||||
tags={data ?? []}
|
||||
loadingTags={isFetching}
|
||||
tags={tags ?? []}
|
||||
loadingTags={false}
|
||||
disabled={false}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ import {
|
|||
import {
|
||||
ADMIN_HEADER_DESCRIPTION,
|
||||
ADMIN_HEADER_TITLE,
|
||||
PAGINATION_PAGE,
|
||||
PAGINATION_ROWS_COUNT,
|
||||
PAGINATION_SIZE,
|
||||
} from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import ConfirmationModal from "../../modals/confirmationModal";
|
||||
|
|
@ -43,8 +46,8 @@ import { UserInputType } from "../../types/components";
|
|||
export default function AdminPage() {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const [size, setPageSize] = useState(12);
|
||||
const [index, setPageIndex] = useState(1);
|
||||
const [size, setPageSize] = useState(PAGINATION_SIZE);
|
||||
const [index, setPageIndex] = useState(PAGINATION_PAGE);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const { userData } = useContext(AuthContext);
|
||||
|
|
@ -64,7 +67,7 @@ export default function AdminPage() {
|
|||
|
||||
const [filterUserList, setFilterUserList] = useState(userList.current);
|
||||
|
||||
const { mutate: mutateGetUsers, isPending } = useGetUsers({});
|
||||
const { mutate: mutateGetUsers, isPending, isIdle } = useGetUsers({});
|
||||
|
||||
function getUsers() {
|
||||
mutateGetUsers(
|
||||
|
|
@ -103,8 +106,8 @@ export default function AdminPage() {
|
|||
}
|
||||
|
||||
function resetFilter() {
|
||||
setPageIndex(1);
|
||||
setPageSize(12);
|
||||
setPageIndex(PAGINATION_PAGE);
|
||||
setPageSize(PAGINATION_SIZE);
|
||||
getUsers();
|
||||
}
|
||||
|
||||
|
|
@ -298,11 +301,11 @@ export default function AdminPage() {
|
|||
</UserManagementModal>
|
||||
</div>
|
||||
</div>
|
||||
{isPending ? (
|
||||
{isPending || isIdle ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoadingComponent remSize={12} />
|
||||
</div>
|
||||
) : userList.current.length === 0 ? (
|
||||
) : userList.current.length === 0 && !isIdle ? (
|
||||
<>
|
||||
<div className="m-4 flex items-center justify-between text-sm">
|
||||
No users registered.
|
||||
|
|
@ -312,7 +315,7 @@ export default function AdminPage() {
|
|||
<>
|
||||
<div
|
||||
className={
|
||||
"m-4 h-full overflow-x-hidden overflow-y-scroll rounded-md border-2 bg-background custom-scroll" +
|
||||
"m-4 h-fit overflow-x-hidden overflow-y-scroll rounded-md border-2 bg-background custom-scroll" +
|
||||
(isPending ? " border-0" : "")
|
||||
}
|
||||
>
|
||||
|
|
@ -488,9 +491,8 @@ export default function AdminPage() {
|
|||
pageIndex={index}
|
||||
pageSize={size}
|
||||
totalRowsCount={totalRowsCount}
|
||||
paginate={(pageSize, pageIndex) => {
|
||||
handleChangePagination(pageIndex, pageSize);
|
||||
}}
|
||||
paginate={handleChangePagination}
|
||||
rowsCount={PAGINATION_ROWS_COUNT}
|
||||
></PaginatorComponent>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { useGetAutoLogin } from "@/controllers/API/queries/auth";
|
||||
import { useGetConfig } from "@/controllers/API/queries/config/use-get-config";
|
||||
import { useGetBasicExamplesQuery } from "@/controllers/API/queries/flows/use-get-basic-examples";
|
||||
import { useGetFoldersQuery } from "@/controllers/API/queries/folders/use-get-folders";
|
||||
import { useGetTagsQuery } from "@/controllers/API/queries/store";
|
||||
import { useGetGlobalVariables } from "@/controllers/API/queries/variables";
|
||||
import { useGetVersionQuery } from "@/controllers/API/queries/version";
|
||||
import { CustomLoadingPage } from "@/customization/components/custom-loading-page";
|
||||
|
|
@ -21,10 +24,14 @@ export function AppInitPage() {
|
|||
useGetVersionQuery({ enabled: isFetched });
|
||||
useGetConfig({ enabled: isFetched });
|
||||
useGetGlobalVariables({ enabled: isFetched });
|
||||
useGetBasicExamplesQuery({ enabled: isFetched });
|
||||
useGetTagsQuery({ enabled: isFetched });
|
||||
|
||||
const { refetch: refetchFolders } = useGetFoldersQuery();
|
||||
useEffect(() => {
|
||||
if (isFetched) {
|
||||
refreshStars();
|
||||
refetchFolders();
|
||||
}
|
||||
}, [isFetched]);
|
||||
|
||||
|
|
|
|||
|
|
@ -597,12 +597,13 @@ export default function NodeToolbarComponent({
|
|||
size={"x-small"}
|
||||
icon={"SaveAll"}
|
||||
index={6}
|
||||
onConfirm={(index, user) => {
|
||||
onConfirm={() => {
|
||||
addFlow({
|
||||
flow: flowComponent,
|
||||
override: true,
|
||||
});
|
||||
setSuccessData({ title: `${data.id} successfully overridden!` });
|
||||
setShowOverrideModal(false);
|
||||
}}
|
||||
onClose={() => setShowOverrideModal(false)}
|
||||
onCancel={() => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useGetFlow } from "@/controllers/API/queries/flows/use-get-flow";
|
||||
import { useGetRefreshFlows } from "@/controllers/API/queries/flows/use-get-refresh-flows";
|
||||
import { ENABLE_BRANDING } from "@/customization/feature-flags";
|
||||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
|
|
@ -36,17 +37,20 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
|
||||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
|
||||
|
||||
const flowToCanvas = useFlowsManagerStore((state) => state.flowToCanvas);
|
||||
|
||||
const { mutateAsync: refreshFlows } = useGetRefreshFlows();
|
||||
const setIsLoading = useFlowsManagerStore((state) => state.setIsLoading);
|
||||
const getTypes = useTypesStore((state) => state.getTypes);
|
||||
const types = useTypesStore((state) => state.types);
|
||||
|
||||
const updatedAt = currentSavedFlow?.updated_at;
|
||||
|
||||
const autoSaving = useFlowsManagerStore((state) => state.autoSaving);
|
||||
|
||||
const stopBuilding = useFlowStore((state) => state.stopBuilding);
|
||||
|
||||
const { mutateAsync: getFlow } = useGetFlow();
|
||||
|
||||
const handleSave = () => {
|
||||
let saving = true;
|
||||
let proceed = false;
|
||||
|
|
@ -106,16 +110,20 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
return;
|
||||
}
|
||||
|
||||
setCurrentFlow(isAnExistingFlow);
|
||||
const isAnExistingFlowId = isAnExistingFlow.id;
|
||||
|
||||
flowToCanvas
|
||||
? setCurrentFlow(flowToCanvas)
|
||||
: getFlowToAddToCanvas(isAnExistingFlowId);
|
||||
} else if (!flows) {
|
||||
setIsLoading(true);
|
||||
await refreshFlows(undefined);
|
||||
await refreshFlows({ get_all: true, header_flows: true });
|
||||
if (!types || Object.keys(types).length === 0) await getTypes();
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
awaitgetTypes();
|
||||
}, [id, flows, currentFlowId]);
|
||||
}, [id, flows, currentFlowId, flowToCanvas]);
|
||||
|
||||
useEffect(() => {
|
||||
setOnFlowPage(true);
|
||||
|
|
@ -147,6 +155,11 @@ export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
|
|||
}
|
||||
}, [blocker.state, isBuilding]);
|
||||
|
||||
const getFlowToAddToCanvas = async (id: string) => {
|
||||
const flow = await getFlow({ id: id });
|
||||
setCurrentFlow(flow);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flow-page-positioning">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
import CollectionCardComponent from "../../../../../../components/cardComponent";
|
||||
const CollectionCard = ({ item, type, isLoading, control }) => {
|
||||
|
|
@ -10,8 +11,13 @@ const CollectionCard = ({ item, type, isLoading, control }) => {
|
|||
|
||||
const editFlowLink = `/flow/${item.id}${folderId ? `/folder/${folderId}` : ""}`;
|
||||
|
||||
const handleClick = () => {
|
||||
const setFlowToCanvas = useFlowsManagerStore(
|
||||
(state) => state.setFlowToCanvas,
|
||||
);
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!isComponent) {
|
||||
await setFlowToCanvas(item);
|
||||
navigate(editFlowLink);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { PAGINATION_PAGE, PAGINATION_SIZE } from "@/constants/constants";
|
||||
import { usePostDownloadMultipleFlows } from "@/controllers/API/queries/flows";
|
||||
import NewFlowModal from "@/modals/newFlowModal";
|
||||
import { Pagination } from "@/types/utils/types";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
|
|
@ -28,13 +30,17 @@ import useSelectedFlows from "./hooks/use-selected-flows";
|
|||
export default function ComponentsComponent({
|
||||
type = "all",
|
||||
currentFolder,
|
||||
pagination,
|
||||
isLoading,
|
||||
deleteFlow,
|
||||
onPaginate,
|
||||
}: {
|
||||
type?: string;
|
||||
currentFolder?: FolderType;
|
||||
currentFolder?: FlowType[];
|
||||
isLoading: boolean;
|
||||
pagination: Pagination;
|
||||
deleteFlow: ({ id }: { id: string[] }) => Promise<void>;
|
||||
onPaginate: (pageIndex: number, pageSize: number) => void;
|
||||
}) {
|
||||
const { folderId } = useParams();
|
||||
|
||||
|
|
@ -56,18 +62,12 @@ export default function ComponentsComponent({
|
|||
);
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
|
||||
const flowsFromFolder = currentFolder?.flows ?? [];
|
||||
const flowsFromFolder = currentFolder ?? [];
|
||||
|
||||
const [filteredFlows, setFilteredFlows] =
|
||||
useState<FlowType[]>(flowsFromFolder);
|
||||
|
||||
const handleFileDrop = useFileDrop(type);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const all: FlowType[] = sortFlows(filteredFlows, type);
|
||||
const start = (pageIndex - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const data: FlowType[] = all?.slice(start, end);
|
||||
const location = useLocation();
|
||||
|
||||
const name = getNameByType(type);
|
||||
|
|
@ -93,8 +93,7 @@ export default function ComponentsComponent({
|
|||
useFilteredFlows(flowsFromFolder, searchFlowsComponents, setFilteredFlows);
|
||||
|
||||
const resetFilter = () => {
|
||||
setPageIndex(1);
|
||||
setPageSize(20);
|
||||
onPaginate(PAGINATION_PAGE, PAGINATION_SIZE);
|
||||
};
|
||||
|
||||
const { getValues, control, setValue } = useForm();
|
||||
|
|
@ -206,8 +205,6 @@ export default function ComponentsComponent({
|
|||
type,
|
||||
);
|
||||
|
||||
const totalRowsCount = filteredFlows?.length;
|
||||
|
||||
const handleOpenModal = () => {
|
||||
setOpenModal(true);
|
||||
};
|
||||
|
|
@ -216,7 +213,7 @@ export default function ComponentsComponent({
|
|||
<>
|
||||
<div className="flex w-full gap-4 pb-5">
|
||||
<HeaderComponent
|
||||
disabled={isLoading || data?.length === 0}
|
||||
disabled={isLoading || flowsFromFolder?.length === 0}
|
||||
shouldSelectAll={shouldSelectAll}
|
||||
setShouldSelectAll={setShouldSelectAll}
|
||||
handleDelete={() => handleSelectOptionsChange("delete")}
|
||||
|
|
@ -236,13 +233,13 @@ export default function ComponentsComponent({
|
|||
data-testid="cards-wrapper"
|
||||
>
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
{!isLoading && data?.length === 0 ? (
|
||||
{!isLoading && flowsFromFolder?.length === 0 ? (
|
||||
<EmptyComponent handleOpenModal={handleOpenModal} />
|
||||
) : (
|
||||
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
|
||||
{data?.length > 0 ? (
|
||||
{flowsFromFolder?.length > 0 ? (
|
||||
<>
|
||||
{data?.map((item) => (
|
||||
{flowsFromFolder?.map((item) => (
|
||||
<FormProvider {...methods} key={item.id}>
|
||||
<form>
|
||||
<CollectionCard
|
||||
|
|
@ -264,18 +261,16 @@ export default function ComponentsComponent({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isLoading && data?.length > 0 && (
|
||||
{!isLoading && flowsFromFolder?.length > 0 && (
|
||||
<div className="relative py-6">
|
||||
<PaginatorComponent
|
||||
storeComponent={true}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
pageIndex={pagination.page}
|
||||
pageSize={pagination.size}
|
||||
rowsCount={[10, 20, 50, 100]}
|
||||
totalRowsCount={totalRowsCount}
|
||||
paginate={(pageSize, pageIndex) => {
|
||||
setPageIndex(pageIndex);
|
||||
setPageSize(pageSize);
|
||||
}}
|
||||
totalRowsCount={pagination.total ?? 0}
|
||||
paginate={onPaginate}
|
||||
pages={pagination.pages}
|
||||
></PaginatorComponent>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,48 @@
|
|||
import { useState } from "react";
|
||||
import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore";
|
||||
import { SEARCH_TABS } from "@/constants/constants";
|
||||
import { useCallback, useState } from "react";
|
||||
import InputSearchComponent from "../inputSearchComponent";
|
||||
import TabsSearchComponent from "../tabsComponent";
|
||||
|
||||
type HeaderTabsSearchComponentProps = {
|
||||
loading: boolean;
|
||||
onChangeTab: (tab: string) => void;
|
||||
onSearch: (search: string) => void;
|
||||
activeTab: string;
|
||||
};
|
||||
|
||||
const HeaderTabsSearchComponent = ({
|
||||
loading,
|
||||
onChangeTab,
|
||||
onSearch,
|
||||
activeTab,
|
||||
}: HeaderTabsSearchComponentProps) => {
|
||||
const [tabActive, setTabActive] = useState("Flows");
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
const setSearchFlowsComponents = useFlowsManagerStore(
|
||||
(state) => state.setSearchFlowsComponents,
|
||||
const handleChangeTab = useCallback(
|
||||
(tab: string) => {
|
||||
onChangeTab(tab);
|
||||
},
|
||||
[onChangeTab],
|
||||
);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
onSearch(inputValue);
|
||||
}, [onSearch, inputValue]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
},
|
||||
[handleSearch],
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -23,24 +51,18 @@ const HeaderTabsSearchComponent = ({
|
|||
<InputSearchComponent
|
||||
loading={loading}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setSearchFlowsComponents(e.target.value);
|
||||
setInputValue(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
setSearchFlowsComponents(inputValue);
|
||||
}
|
||||
}}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<TabsSearchComponent
|
||||
tabsOptions={["All", "Flows", "Components"]}
|
||||
setActiveTab={setTabActive}
|
||||
tabsOptions={SEARCH_TABS}
|
||||
setActiveTab={handleChangeTab}
|
||||
loading={loading}
|
||||
tabActive={tabActive}
|
||||
tabActive={activeTab}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderTabsSearchComponent;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
|
||||
type TabsSearchComponentProps = {
|
||||
tabsOptions: string[];
|
||||
|
|
@ -16,59 +16,49 @@ const TabsSearchComponent = ({
|
|||
}: TabsSearchComponentProps) => {
|
||||
const navigate = useCustomNavigate();
|
||||
|
||||
const changeLocation = (tabOption) => {
|
||||
const location = window.location.pathname;
|
||||
let newLocation = "";
|
||||
switch (tabOption) {
|
||||
case "Flows":
|
||||
newLocation = location.replace(/.*\/(?:all|components)/, "/flows");
|
||||
break;
|
||||
case "Components":
|
||||
newLocation = location.replace(/.*\/(?:flows|all)/, "/components");
|
||||
break;
|
||||
default:
|
||||
newLocation = location.replace(/.*\/(?:flows|components)/, "/all");
|
||||
break;
|
||||
}
|
||||
navigate(newLocation);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const path = window.location.pathname;
|
||||
if (path.includes("components")) {
|
||||
setActiveTab("Components");
|
||||
} else if (path.includes("flows")) {
|
||||
setActiveTab("Flows");
|
||||
} else {
|
||||
setActiveTab("All");
|
||||
}
|
||||
}, [window.location.pathname]);
|
||||
const changeLocation = useCallback(
|
||||
(tabOption: string) => {
|
||||
const location = window.location.pathname;
|
||||
let newLocation = "";
|
||||
switch (tabOption) {
|
||||
case "Flows":
|
||||
newLocation = location.replace(/.*\/(?:all|components)/, "/flows");
|
||||
break;
|
||||
case "Components":
|
||||
newLocation = location.replace(/.*\/(?:flows|all)/, "/components");
|
||||
break;
|
||||
default:
|
||||
newLocation = location.replace(/.*\/(?:flows|components)/, "/all");
|
||||
break;
|
||||
}
|
||||
navigate(newLocation);
|
||||
setActiveTab(tabOption);
|
||||
},
|
||||
[navigate, setActiveTab],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="ml-4 flex w-full gap-2 border-b border-border">
|
||||
{tabsOptions.map((tabOption, index) => {
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
data-testid={`${tabOption}-button-store`}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
changeLocation(tabOption);
|
||||
}}
|
||||
className={
|
||||
(tabActive === tabOption
|
||||
? "border-b-2 border-primary p-3"
|
||||
: "border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
|
||||
(loading ? " cursor-not-allowed" : "")
|
||||
}
|
||||
>
|
||||
{tabOption}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{tabsOptions.map((tabOption, index) => (
|
||||
<button
|
||||
key={index}
|
||||
data-testid={`${tabOption}-button-store`}
|
||||
disabled={loading}
|
||||
onClick={() => changeLocation(tabOption)}
|
||||
className={
|
||||
(tabActive === tabOption
|
||||
? "border-b-2 border-primary p-3"
|
||||
: "border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
|
||||
(loading ? " cursor-not-allowed" : "")
|
||||
}
|
||||
>
|
||||
{tabOption}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabsSearchComponent;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useGetFolderQuery } from "@/controllers/API/queries/folders/use-get-folder";
|
||||
import useDeleteFlow from "@/hooks/flows/use-delete-flow";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import { useFolderStore } from "@/stores/foldersStore";
|
||||
import { useIsFetching, useIsMutating } from "@tanstack/react-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useLocation, useParams } from "react-router-dom";
|
||||
import ComponentsComponent from "../componentsComponent";
|
||||
import HeaderTabsSearchComponent from "./components/headerTabsSearchComponent";
|
||||
|
||||
|
|
@ -13,27 +13,26 @@ type MyCollectionComponentProps = {
|
|||
|
||||
const MyCollectionComponent = ({ type }: MyCollectionComponentProps) => {
|
||||
const { folderId } = useParams();
|
||||
const location = useLocation();
|
||||
const myCollectionId = useFolderStore((state) => state.myCollectionId);
|
||||
|
||||
const flows = useFlowsManagerStore((state) => state.flows);
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [filter, setFilter] = useState<string>(() => {
|
||||
if (location.pathname.includes("components")) return "Components";
|
||||
if (location.pathname.includes("flows")) return "Flows";
|
||||
return "All";
|
||||
});
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
const { data: folderData, isFetching } = useGetFolderQuery(
|
||||
{
|
||||
id: folderId ?? myCollectionId ?? "",
|
||||
},
|
||||
{ enabled: !!folderId || !!myCollectionId },
|
||||
);
|
||||
|
||||
const data = {
|
||||
flows:
|
||||
folderData?.flows.filter((flow) =>
|
||||
flows?.find((f) => f.id === flow.id),
|
||||
) ?? [],
|
||||
name: folderData?.name ?? "",
|
||||
description: folderData?.description ?? "",
|
||||
parent_id: folderData?.parent_id ?? "",
|
||||
components: folderData?.components ?? [],
|
||||
};
|
||||
const { data: folderData, isFetching } = useGetFolderQuery({
|
||||
id: folderId ?? myCollectionId!,
|
||||
page: pageIndex,
|
||||
size: pageSize,
|
||||
is_component: filter === "Components",
|
||||
is_flow: filter === "Flows",
|
||||
search: search,
|
||||
});
|
||||
|
||||
const isLoadingFolders = !!useIsFetching({
|
||||
queryKey: ["useGetFolders"],
|
||||
|
|
@ -47,23 +46,60 @@ const MyCollectionComponent = ({ type }: MyCollectionComponentProps) => {
|
|||
exact: true,
|
||||
});
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(newPageIndex: number, newPageSize: number) => {
|
||||
setPageIndex(newPageIndex);
|
||||
setPageSize(newPageSize);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onChangeTab = useCallback((newFilter: string) => {
|
||||
setFilter(newFilter);
|
||||
setPageIndex(1);
|
||||
}, []);
|
||||
|
||||
const onSearch = useCallback((newSearch: string) => {
|
||||
setSearch(newSearch);
|
||||
setPageIndex(1);
|
||||
}, []);
|
||||
|
||||
const data = {
|
||||
flows: folderData?.flows?.items ?? [],
|
||||
name: folderData?.folder?.name ?? "",
|
||||
description: folderData?.folder?.description ?? "",
|
||||
parent_id: folderData?.folder?.parent_id ?? "",
|
||||
components: folderData?.folder?.components ?? [],
|
||||
pagination: {
|
||||
page: folderData?.flows?.page ?? 1,
|
||||
size: folderData?.flows?.size ?? 10,
|
||||
total: folderData?.flows?.total ?? 0,
|
||||
pages: folderData?.flows?.pages ?? 0,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderTabsSearchComponent
|
||||
loading={isFetching || isLoadingFolders || isDeleting || isAddingFlow}
|
||||
onChangeTab={onChangeTab}
|
||||
onSearch={onSearch}
|
||||
activeTab={filter}
|
||||
/>
|
||||
<div className="mt-5 flex h-full flex-col">
|
||||
<ComponentsComponent
|
||||
key={type}
|
||||
type={type}
|
||||
currentFolder={data}
|
||||
currentFolder={data.flows}
|
||||
pagination={data.pagination}
|
||||
deleteFlow={deleteFlow}
|
||||
isLoading={
|
||||
isFetching || isLoadingFolders || isDeleting || isAddingFlow
|
||||
}
|
||||
onPaginate={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyCollectionComponent;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,23 @@ export type FolderType = {
|
|||
components: string[];
|
||||
};
|
||||
|
||||
export type PaginatedFolderType = {
|
||||
folder: {
|
||||
name: string;
|
||||
description: string;
|
||||
id?: string | null;
|
||||
parent_id: string;
|
||||
components: string[];
|
||||
};
|
||||
flows: {
|
||||
items: FlowType[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
pages: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type AddFolderType = {
|
||||
name: string;
|
||||
description: string;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,13 @@ export default function HomePage(): JSX.Element {
|
|||
exact: false,
|
||||
});
|
||||
|
||||
const isFetchingFolder = !!useIsFetching({
|
||||
queryKey: ["useGetFolder"],
|
||||
exact: false,
|
||||
});
|
||||
|
||||
const isLoadingFolder = isFetchingFolders || isFetchingFolder;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageLayout
|
||||
|
|
@ -86,7 +93,7 @@ export default function HomePage(): JSX.Element {
|
|||
options={dropdownOptions}
|
||||
plusButton={true}
|
||||
dropdownOptions={false}
|
||||
isFetchingFolders={isFetchingFolders}
|
||||
isFetchingFolders={isLoadingFolder}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export default function PlaygroundPage() {
|
|||
setCurrentFlow(isAnExistingFlow);
|
||||
} else if (!flows) {
|
||||
setIsLoading(true);
|
||||
await refreshFlows(undefined);
|
||||
await refreshFlows({ get_all: true, header_flows: true });
|
||||
if (!types || Object.keys(types).length === 0) await getTypes();
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
|||
import { Button } from "../../components/ui/button";
|
||||
|
||||
import StoreCardComponent from "@/components/storeCardComponent";
|
||||
import { useGetTagsQuery } from "@/controllers/API/queries/store";
|
||||
import { CustomLink } from "@/customization/components/custom-link";
|
||||
import { useCustomNavigate } from "@/customization/hooks/use-custom-navigate";
|
||||
import { useUtilityStore } from "@/stores/utilityStore";
|
||||
import { useParams } from "react-router-dom";
|
||||
import PaginatorComponent from "../../components/paginatorComponent";
|
||||
import { TagsSelector } from "../../components/tagsSelectorComponent";
|
||||
|
|
@ -28,7 +28,13 @@ import {
|
|||
INVALID_API_ERROR_ALERT,
|
||||
NOAPI_ERROR_ALERT,
|
||||
} from "../../constants/alerts_constants";
|
||||
import { STORE_DESC, STORE_TITLE } from "../../constants/constants";
|
||||
import {
|
||||
STORE_DESC,
|
||||
STORE_PAGINATION_PAGE,
|
||||
STORE_PAGINATION_ROWS_COUNT,
|
||||
STORE_PAGINATION_SIZE,
|
||||
STORE_TITLE,
|
||||
} from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { getStoreComponents } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
|
|
@ -55,13 +61,14 @@ export default function StorePage(): JSX.Element {
|
|||
const [inputText, setInputText] = useState<string>("");
|
||||
const [searchData, setSearchData] = useState<storeComponent[]>([]);
|
||||
const [totalRowsCount, setTotalRowsCount] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(12);
|
||||
const [pageIndex, setPageIndex] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(STORE_PAGINATION_SIZE);
|
||||
const [pageIndex, setPageIndex] = useState(STORE_PAGINATION_PAGE);
|
||||
const [pageOrder, setPageOrder] = useState("Popular");
|
||||
const [tabActive, setTabActive] = useState("All");
|
||||
const [searchNow, setSearchNow] = useState("");
|
||||
const [selectFilter, setSelectFilter] = useState("all");
|
||||
const { isFetching, data } = useGetTagsQuery();
|
||||
|
||||
const tags = useUtilityStore((state) => state.tags);
|
||||
|
||||
const navigate = useCustomNavigate();
|
||||
|
||||
|
|
@ -148,8 +155,8 @@ export default function StorePage(): JSX.Element {
|
|||
}
|
||||
|
||||
function resetPagination() {
|
||||
setPageIndex(1);
|
||||
setPageSize(12);
|
||||
setPageIndex(STORE_PAGINATION_PAGE);
|
||||
setPageSize(STORE_PAGINATION_SIZE);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -277,8 +284,8 @@ export default function StorePage(): JSX.Element {
|
|||
</Select>
|
||||
{id === undefined ? (
|
||||
<TagsSelector
|
||||
tags={data ?? []}
|
||||
loadingTags={isFetching}
|
||||
tags={tags ?? []}
|
||||
loadingTags={false}
|
||||
disabled={loading}
|
||||
selectedTags={filteredCategories}
|
||||
setSelectedTags={setFilterCategories}
|
||||
|
|
@ -376,8 +383,9 @@ export default function StorePage(): JSX.Element {
|
|||
storeComponent={true}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
rowsCount={STORE_PAGINATION_ROWS_COUNT}
|
||||
totalRowsCount={totalRowsCount}
|
||||
paginate={(pageSize, pageIndex) => {
|
||||
paginate={(pageIndex, pageSize) => {
|
||||
setPageIndex(pageIndex);
|
||||
setPageSize(pageSize);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function ViewPage() {
|
|||
setCurrentFlow(isAnExistingFlow);
|
||||
} else if (!flows) {
|
||||
setIsLoading(true);
|
||||
await refreshFlows(undefined);
|
||||
await refreshFlows({ get_all: true, header_flows: true });
|
||||
if (!types || Object.keys(types).length === 0) await getTypes();
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,13 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
setSelectedFlowsComponentsCards: (selectedFlowsComponentsCards: string[]) => {
|
||||
set({ selectedFlowsComponentsCards });
|
||||
},
|
||||
flowToCanvas: null,
|
||||
setFlowToCanvas: async (flowToCanvas: FlowType | null) => {
|
||||
await new Promise<void>((resolve) => {
|
||||
set({ flowToCanvas });
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
export default useFlowsManagerStore;
|
||||
|
|
|
|||
|
|
@ -15,4 +15,6 @@ export const useFolderStore = create<FoldersStoreType>((set, get) => ({
|
|||
setFolderIdDragging: (id) => set(() => ({ folderIdDragging: id })),
|
||||
starterProjectId: "",
|
||||
setStarterProjectId: (id) => set(() => ({ starterProjectId: id })),
|
||||
folders: [],
|
||||
setFolders: (folders) => set(() => ({ folders: folders })),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Pagination, Tag } from "@/types/utils/types";
|
||||
import { UtilityStoreType } from "@/types/zustand/utility";
|
||||
import { create } from "zustand";
|
||||
|
||||
|
|
@ -21,4 +22,11 @@ export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
|
|||
maxFileSizeUpload: 100 * 1024 * 1024, // 100MB in bytes
|
||||
setMaxFileSizeUpload: (maxFileSizeUpload: number) =>
|
||||
set({ maxFileSizeUpload: maxFileSizeUpload * 1024 * 1024 }),
|
||||
flowsPagination: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
},
|
||||
setFlowsPagination: (flowsPagination: Pagination) => set({ flowsPagination }),
|
||||
tags: [],
|
||||
setTags: (tags: Tag[]) => set({ tags }),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -298,6 +298,7 @@ export type PaginatorComponentType = {
|
|||
totalRowsCount: number;
|
||||
paginate: (pageIndex: number, pageSize: number) => void;
|
||||
storeComponent?: boolean;
|
||||
pages?: number;
|
||||
};
|
||||
|
||||
export type ConfirmationModalType = {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@ import { ReactFlowJsonObject, XYPosition } from "reactflow";
|
|||
import { BuildStatus } from "../../constants/enums";
|
||||
import { APIClassType } from "../api/index";
|
||||
|
||||
export type PaginatedFlowsType = {
|
||||
items: FlowType[];
|
||||
total: number;
|
||||
size: number;
|
||||
page: number;
|
||||
pages: number;
|
||||
};
|
||||
|
||||
export type FlowType = {
|
||||
name: string;
|
||||
id: string;
|
||||
|
|
|
|||
11
src/frontend/src/types/utils/types.ts
Normal file
11
src/frontend/src/types/utils/types.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export type Pagination = {
|
||||
total?: number;
|
||||
size: number;
|
||||
page: number;
|
||||
pages?: number;
|
||||
};
|
||||
|
||||
export type Tag = {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
|
|
@ -26,6 +26,8 @@ export type FlowsManagerStoreType = {
|
|||
setAutoSavingInterval: (autoSavingInterval: number) => void;
|
||||
healthCheckMaxRetries: number;
|
||||
setHealthCheckMaxRetries: (healthCheckMaxRetries: number) => void;
|
||||
flowToCanvas: FlowType | null;
|
||||
setFlowToCanvas: (flowToCanvas: FlowType | null) => Promise<void>;
|
||||
};
|
||||
|
||||
export type UseUndoRedoOptions = {
|
||||
|
|
|
|||
|
|
@ -11,4 +11,6 @@ export type FoldersStoreType = {
|
|||
setFolderIdDragging: (id: string) => void;
|
||||
starterProjectId: string;
|
||||
setStarterProjectId: (id: string) => void;
|
||||
folders: FolderType[];
|
||||
setFolders: (folders: FolderType[]) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Pagination, Tag } from "@/types/utils/types";
|
||||
|
||||
export type UtilityStoreType = {
|
||||
selectedItems: any[];
|
||||
setSelectedItems: (itemId: any) => void;
|
||||
|
|
@ -7,4 +9,8 @@ export type UtilityStoreType = {
|
|||
setPlaygroundScrollBehaves: (behaves: ScrollBehavior) => void;
|
||||
maxFileSizeUpload: number;
|
||||
setMaxFileSizeUpload: (maxFileSizeUpload: number) => void;
|
||||
flowsPagination: Pagination;
|
||||
setFlowsPagination: (pagination: Pagination) => void;
|
||||
tags: Tag[];
|
||||
setTags: (tags: Tag[]) => void;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,11 +35,11 @@ test("CRUD folders", async ({ page }) => {
|
|||
await page.getByTestId("icon-ChevronLeft").first().click();
|
||||
|
||||
await page.getByText("My Collection").nth(2).isVisible();
|
||||
await page.getByPlaceholder("Search flows").isVisible();
|
||||
await page.getByText("Flows").isVisible();
|
||||
await page.getByText("Components").isVisible();
|
||||
await page.getByPlaceholder("Search flows").first().isVisible();
|
||||
await page.getByText("Flows").first().isVisible();
|
||||
await page.getByText("Components").first().isVisible();
|
||||
await page.getByText("All").first().isVisible();
|
||||
await page.getByText("Select All").isVisible();
|
||||
await page.getByText("Select All").first().isVisible();
|
||||
|
||||
await page.getByTestId("add-folder-button").click();
|
||||
await page.getByText("New Folder").last().isVisible();
|
||||
|
|
@ -111,7 +111,7 @@ test("add a flow into a folder by drag and drop", async ({ page }) => {
|
|||
|
||||
await page.getByTestId("sidebar-nav-My Projects").click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
expect(
|
||||
await page.locator("text=Getting Started:").last().isVisible(),
|
||||
|
|
@ -162,10 +162,10 @@ test("change flow folder", async ({ page }) => {
|
|||
|
||||
await page.getByText("My Collection").nth(2).isVisible();
|
||||
await page.getByPlaceholder("Search flows").isVisible();
|
||||
await page.getByText("Flows").isVisible();
|
||||
await page.getByText("Components").isVisible();
|
||||
await page.getByText("Flows").first().isVisible();
|
||||
await page.getByText("Components").first().isVisible();
|
||||
await page.getByText("All").first().isVisible();
|
||||
await page.getByText("Select All").isVisible();
|
||||
await page.getByText("Select All").first().isVisible();
|
||||
|
||||
await page.getByTestId("add-folder-button").click();
|
||||
await page.getByText("New Folder").last().isVisible();
|
||||
|
|
|
|||
|
|
@ -71,7 +71,20 @@ test("user should be able to manually save a flow when the auto_save is off", as
|
|||
|
||||
await page.getByTitle("fit view").click();
|
||||
|
||||
expect(await page.getByText("Last saved:").isVisible()).toBeTruthy();
|
||||
expect(await page.getByText("Saved").isVisible()).toBeTruthy();
|
||||
|
||||
await page
|
||||
.getByText("Saved")
|
||||
.first()
|
||||
.hover()
|
||||
.then(async () => {
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByText("Auto-saving is disabled").nth(0).isVisible();
|
||||
await page
|
||||
.getByText("Enable auto-saving to avoid losing progress.")
|
||||
.nth(0)
|
||||
.isVisible();
|
||||
});
|
||||
|
||||
expect(await page.getByTestId("save-flow-button").isEnabled()).toBeTruthy();
|
||||
|
||||
|
|
|
|||
15
uv.lock
generated
15
uv.lock
generated
|
|
@ -1704,6 +1704,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/06/ab/a1f7eed031aeb1c406a6e9d45ca04bff401c8a25a30dd0e4fd2caae767c3/fastapi-0.115.0-py3-none-any.whl", hash = "sha256:17ea427674467486e997206a5ab25760f6b09e069f099b96f5b55a32fb6f1631", size = 94625 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-pagination"
|
||||
version = "0.12.29"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/4f/758832457733a75522fe6693b543a1404009bfa19fd5198e80cf6f77f232/fastapi_pagination-0.12.29.tar.gz", hash = "sha256:bbfa8af44636f9cda3ec10f59fe370a89a8f10cfa4035a6dc4da86b9f8d06649", size = 26279 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/8f/3a0ab35dcb73c8e93a7dc66ff0641bcc4af2a6b919ba21f3c1873c642f96/fastapi_pagination-0.12.29-py3-none-any.whl", hash = "sha256:c18e58c91cf7909490236ded4dae1326c68e286a0831de41ba1be170fb24f182", size = 42525 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastavro"
|
||||
version = "1.9.7"
|
||||
|
|
@ -3750,6 +3763,7 @@ dependencies = [
|
|||
{ name = "duckdb" },
|
||||
{ name = "emoji" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "fastapi-pagination" },
|
||||
{ name = "filelock" },
|
||||
{ name = "firecrawl-py" },
|
||||
{ name = "grandalf" },
|
||||
|
|
@ -3871,6 +3885,7 @@ requires-dist = [
|
|||
{ name = "duckdb", specifier = ">=1.0.0" },
|
||||
{ name = "emoji", specifier = ">=2.12.0" },
|
||||
{ name = "fastapi", specifier = ">=0.111.0" },
|
||||
{ name = "fastapi-pagination", specifier = ">=0.12.29" },
|
||||
{ name = "filelock", specifier = ">=3.15.4" },
|
||||
{ name = "firecrawl-py", specifier = ">=0.0.16" },
|
||||
{ name = "flower", marker = "extra == 'all'", specifier = ">=1.0.0" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue