Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/server/src/controllers/chatflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ const deleteChatflow = async (req: Request, res: Response, next: NextFunction) =
const getAllChatflows = async (req: Request, res: Response, next: NextFunction) => {
try {
const { page, limit } = getPageAndLimitParams(req)
const search = typeof req.query?.search === 'string' ? req.query.search : undefined

const apiResponse = await chatflowsService.getAllChatflows(
req.query?.type as ChatflowType,
req.user?.activeWorkspaceId,
page,
limit
limit,
search
)
return res.json(apiResponse)
} catch (error) {
Expand Down
26 changes: 21 additions & 5 deletions packages/server/src/services/chatflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,20 @@ const deleteChatflow = async (chatflowId: string, orgId: string, workspaceId: st
}
}

const getAllChatflows = async (type?: ChatflowType, workspaceId?: string, page: number = -1, limit: number = -1) => {
const getAllChatflows = async (
type?: ChatflowType,
workspaceId?: string,
page: number = -1,
limit: number = -1,
search?: string
) => {
try {
const appServer = getRunningExpressApp()

const queryBuilder = appServer.AppDataSource.getRepository(ChatFlow)
.createQueryBuilder('chat_flow')
.orderBy('chat_flow.updatedDate', 'DESC')

if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
if (type === 'MULTIAGENT') {
queryBuilder.andWhere('chat_flow.type = :type', { type: 'MULTIAGENT' })
} else if (type === 'AGENTFLOW') {
Expand All @@ -165,6 +167,20 @@ const getAllChatflows = async (type?: ChatflowType, workspaceId?: string, page:
queryBuilder.andWhere('chat_flow.type = :type', { type: 'CHATFLOW' })
}
if (workspaceId) queryBuilder.andWhere('chat_flow.workspaceId = :workspaceId', { workspaceId })
if (search?.trim()) {
const searchTerm = `%${search.trim().toLowerCase()}%`
queryBuilder.andWhere(
new Brackets((qb) => {
qb.where('LOWER(chat_flow.name) LIKE :search', { search: searchTerm })
.orWhere("LOWER(COALESCE(chat_flow.category, '')) LIKE :search", { search: searchTerm })
.orWhere('CAST(chat_flow.id AS TEXT) LIKE :search', { search: searchTerm })
})
)
}
if (page > 0 && limit > 0) {
queryBuilder.skip((page - 1) * limit)
queryBuilder.take(limit)
}
const [data, total] = await queryBuilder.getManyAndCount()

if (page > 0 && limit > 0) {
Expand Down
41 changes: 28 additions & 13 deletions packages/ui/src/views/agentflows/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'

Expand Down Expand Up @@ -42,12 +42,14 @@ const Agentflows = () => {
const [images, setImages] = useState({})
const [icons, setIcons] = useState({})
const [search, setSearch] = useState('')
const [searchQuery, setSearchQuery] = useState('')
const { error, setError } = useError()

const getAllAgentflows = useApi(chatflowsApi.getAllAgentflows)
const [view, setView] = useState(localStorage.getItem('agentFlowDisplayStyle') || 'card')
const [agentflowVersion, setAgentflowVersion] = useState(localStorage.getItem('agentFlowVersion') || 'v2')
const [showDeprecationNotice, setShowDeprecationNotice] = useState(true)
const latestAgentflowVersionRef = useRef(agentflowVersion)

/* Table Pagination */
const [currentPage, setCurrentPage] = useState(1)
Expand All @@ -61,11 +63,12 @@ const Agentflows = () => {
refresh(page, pageLimit, agentflowVersion)
}

const refresh = (page, limit, nextView) => {
const refresh = (page, limit, nextView, nextSearch = searchQuery) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
if (nextSearch) params.search = nextSearch
getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT', params)
}

Expand All @@ -79,21 +82,14 @@ const Agentflows = () => {
if (nextView === null) return
localStorage.setItem('agentFlowVersion', nextView)
setAgentflowVersion(nextView)
setCurrentPage(1)
refresh(1, pageLimit, nextView)
}

const onSearchChange = (event) => {
setSearch(event.target.value)
}

function filterFlows(data) {
return (
data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 ||
(data.category && data.category.toLowerCase().indexOf(search.toLowerCase()) > -1) ||
data.id.toLowerCase().indexOf(search.toLowerCase()) > -1
)
}

const addNew = () => {
if (agentflowVersion === 'v2') {
navigate('/v2/agentcanvas')
Expand All @@ -120,6 +116,25 @@ const Agentflows = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

useEffect(() => {
latestAgentflowVersionRef.current = agentflowVersion
}, [agentflowVersion])

useEffect(() => {
const normalizedSearch = search.trim()
if (normalizedSearch === searchQuery) return

const timeoutId = setTimeout(() => {
setSearchQuery(normalizedSearch)
setCurrentPage(1)
refresh(1, pageLimit, latestAgentflowVersionRef.current, normalizedSearch)
}, 300)
Comment on lines +127 to +131
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The refresh call inside this setTimeout closure captures the value of agentflowVersion from the render cycle when the effect was triggered. If a user types a search term and then quickly switches the Agentflow version (V1/V2) before the 300ms debounce period ends, the search request will be sent for the old version instead of the currently selected one.

To fix this, you should include agentflowVersion in the useEffect dependency array so the debounce is reset and uses the latest version, or use a React Ref to track the current version without triggering re-effects.

References
  1. While repository rules allow omitting dependencies for initial content loading to avoid infinite loops, dynamic interactions like search debouncing must include all relevant dependencies to avoid stale closures and ensure data consistency.


return () => clearTimeout(timeoutId)

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search])

useEffect(() => {
if (getAllAgentflows.error) {
setError(getAllAgentflows.error)
Expand Down Expand Up @@ -178,7 +193,7 @@ const Agentflows = () => {
<ViewHeader
onSearchChange={onSearchChange}
search={true}
searchPlaceholder='Search Name or Category'
searchPlaceholder='Search Name, Category, or ID'
title='Agentflows'
description='Multi-agent systems, workflow orchestration'
>
Expand Down Expand Up @@ -305,7 +320,7 @@ const Agentflows = () => {
<>
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => (
{getAllAgentflows.data?.data.map((data, index) => (
<ItemCard
key={index}
onClick={() => goToCanvas(data)}
Expand All @@ -323,7 +338,7 @@ const Agentflows = () => {
images={images}
icons={icons}
isLoading={isLoading}
filterFunction={filterFlows}
filterFunction={() => true}
updateFlowsApi={getAllAgentflows}
setError={setError}
currentPage={currentPage}
Expand Down