diff --git a/packages/server/src/controllers/chatflows/index.ts b/packages/server/src/controllers/chatflows/index.ts index 6b1a554caa9..4238136bb6d 100644 --- a/packages/server/src/controllers/chatflows/index.ts +++ b/packages/server/src/controllers/chatflows/index.ts @@ -72,12 +72,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) { diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts index 17456bc13e9..ffee33d4b82 100644 --- a/packages/server/src/services/chatflows/index.ts +++ b/packages/server/src/services/chatflows/index.ts @@ -138,7 +138,7 @@ 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() @@ -146,10 +146,6 @@ const getAllChatflows = async (type?: ChatflowType, workspaceId?: string, page: .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') { @@ -161,6 +157,19 @@ 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 }) + + // Apply search filter before pagination + if (search) { + queryBuilder.andWhere( + '(LOWER(chat_flow.name) LIKE :search OR LOWER(chat_flow.category) LIKE :search OR LOWER(chat_flow.id) LIKE :search)', + { search: `%${search.toLowerCase()}%` } + ) + } + + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } const [data, total] = await queryBuilder.getManyAndCount() if (page > 0 && limit > 0) { diff --git a/packages/ui/src/ui-component/table/FlowListTable.jsx b/packages/ui/src/ui-component/table/FlowListTable.jsx index a61df9ed2ed..eeef8642a75 100644 --- a/packages/ui/src/ui-component/table/FlowListTable.jsx +++ b/packages/ui/src/ui-component/table/FlowListTable.jsx @@ -50,17 +50,7 @@ const getLocalStorageKeyName = (name, isAgentCanvas) => { return (isAgentCanvas ? 'agentcanvas' : 'chatflowcanvas') + '_' + name } -export const FlowListTable = ({ - data, - images = {}, - icons = {}, - isLoading, - filterFunction, - updateFlowsApi, - setError, - isAgentCanvas, - isAgentflowV2 -}) => { +export const FlowListTable = ({ data, images = {}, icons = {}, isLoading, updateFlowsApi, setError, isAgentCanvas, isAgentflowV2 }) => { const { hasPermission } = useAuth() const isActionsAvailable = isAgentCanvas ? hasPermission('agentflows:update,agentflows:delete,agentflows:config,agentflows:domains,templates:flowexport,agentflows:export') @@ -186,7 +176,7 @@ export const FlowListTable = ({ ) : ( <> - {sortedData.filter(filterFunction).map((row, index) => ( + {sortedData.map((row, index) => ( diff --git a/packages/ui/src/views/agentflows/index.jsx b/packages/ui/src/views/agentflows/index.jsx index b82bfab8a76..9794ea1cdaa 100644 --- a/packages/ui/src/views/agentflows/index.jsx +++ b/packages/ui/src/views/agentflows/index.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { useSelector } from 'react-redux' @@ -54,17 +54,24 @@ const Agentflows = () => { const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE) const [total, setTotal] = useState(0) + const searchTimeoutRef = useRef(null) + const onChange = (page, pageLimit) => { setCurrentPage(page) setPageLimit(pageLimit) refresh(page, pageLimit, agentflowVersion) } - const refresh = (page, limit, nextView) => { + const refresh = (page, limit, nextView, searchValue) => { const params = { page: page || currentPage, limit: limit || pageLimit } + // Always include search parameter, even if empty (to clear server-side filter) + const currentSearch = searchValue !== undefined ? searchValue : search + if (currentSearch) { + params.search = currentSearch + } getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT', params) } @@ -82,15 +89,19 @@ const Agentflows = () => { } const onSearchChange = (event) => { - setSearch(event.target.value) - } + const newSearch = event.target.value + setSearch(newSearch) + setCurrentPage(1) - 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 - ) + // Clear existing timeout + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current) + } + + // Debounce search - trigger refresh after user stops typing + searchTimeoutRef.current = setTimeout(() => { + refresh(1, pageLimit, agentflowVersion, newSearch) + }, 300) } const addNew = () => { @@ -116,6 +127,12 @@ const Agentflows = () => { useEffect(() => { refresh(currentPage, pageLimit, agentflowVersion) + // Cleanup timeout on unmount + return () => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current) + } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -304,7 +321,7 @@ const Agentflows = () => { <> {!view || view === 'card' ? ( - {getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => ( + {getAllAgentflows.data?.data.map((data, index) => ( goToCanvas(data)} @@ -322,7 +339,6 @@ const Agentflows = () => { images={images} icons={icons} isLoading={isLoading} - filterFunction={filterFlows} updateFlowsApi={getAllAgentflows} setError={setError} /> diff --git a/packages/ui/src/views/chatflows/index.jsx b/packages/ui/src/views/chatflows/index.jsx index 62e855f2b3e..dccc1449d9e 100644 --- a/packages/ui/src/views/chatflows/index.jsx +++ b/packages/ui/src/views/chatflows/index.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState, useRef } from 'react' import { useNavigate } from 'react-router-dom' // material-ui @@ -49,17 +49,24 @@ const Chatflows = () => { const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE) const [total, setTotal] = useState(0) + const searchTimeoutRef = useRef(null) + const onChange = (page, pageLimit) => { setCurrentPage(page) setPageLimit(pageLimit) applyFilters(page, pageLimit) } - const applyFilters = (page, limit) => { + const applyFilters = (page, limit, searchValue) => { const params = { page: page || currentPage, limit: limit || pageLimit } + // Always include search parameter, even if empty (to clear server-side filter) + const currentSearch = searchValue !== undefined ? searchValue : search + if (currentSearch) { + params.search = currentSearch + } getAllChatflowsApi.request(params) } @@ -70,15 +77,19 @@ const Chatflows = () => { } const onSearchChange = (event) => { - setSearch(event.target.value) - } + const newSearch = event.target.value + setSearch(newSearch) + setCurrentPage(1) + + // Clear existing timeout + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current) + } - 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 - ) + // Debounce search - trigger refresh after user stops typing + searchTimeoutRef.current = setTimeout(() => { + applyFilters(1, pageLimit, newSearch) + }, 300) } const addNew = () => { @@ -91,6 +102,13 @@ const Chatflows = () => { useEffect(() => { applyFilters(currentPage, pageLimit) + + // Cleanup timeout on unmount + return () => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current) + } + } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -196,7 +214,7 @@ const Chatflows = () => { <> {!view || view === 'card' ? ( - {getAllChatflowsApi.data?.data?.filter(filterFlows).map((data, index) => ( + {getAllChatflowsApi.data?.data?.map((data, index) => ( goToCanvas(data)} data={data} images={images[data.id]} /> ))} @@ -205,7 +223,6 @@ const Chatflows = () => { data={getAllChatflowsApi.data?.data} images={images} isLoading={isLoading} - filterFunction={filterFlows} updateFlowsApi={getAllChatflowsApi} setError={setError} />