refactor(search): improve SQL search handling and optimize project list component

- Enhanced search handling in WorklenzControllerBase to properly escape single quotes, preventing SQL syntax errors.
- Refactored search logic in ProjectList to maintain reference stability and improve performance during debounced searches.
- Removed unnecessary console logs and optimized loading state management for better user experience.
This commit is contained in:
chamiakJ
2025-07-09 22:38:58 +05:30
parent 8f5de8f1a1
commit 75c55fff21
2 changed files with 62 additions and 74 deletions

View File

@@ -34,29 +34,24 @@ export default abstract class WorklenzControllerBase {
const offset = queryParams.search ? 0 : (index - 1) * size; const offset = queryParams.search ? 0 : (index - 1) * size;
const paging = queryParams.paging || "true"; const paging = queryParams.paging || "true";
// let s = "";
// if (typeof searchField === "string") {
// s = `${searchField} || ' ' || id::TEXT`;
// } else if (Array.isArray(searchField)) {
// s = searchField.join(" || ' ' || ");
// }
// const search = (queryParams.search as string || "").trim();
// const searchQuery = search ? `AND TO_TSVECTOR(${s}) @@ TO_TSQUERY('${toTsQuery(search)}')` : "";
const search = (queryParams.search as string || "").trim(); const search = (queryParams.search as string || "").trim();
let s = "";
if (typeof searchField === "string") {
s = ` ${searchField} ILIKE '%${search}%'`;
} else if (Array.isArray(searchField)) {
s = searchField.map(index => ` ${index} ILIKE '%${search}%'`).join(" OR ");
}
let searchQuery = ""; let searchQuery = "";
if (search) { if (search) {
searchQuery = isMemberFilter ? ` (${s}) AND ` : ` AND (${s}) `; // Properly escape single quotes to prevent SQL syntax errors
const escapedSearch = search.replace(/'/g, "''");
let s = "";
if (typeof searchField === "string") {
s = ` ${searchField} ILIKE '%${escapedSearch}%'`;
} else if (Array.isArray(searchField)) {
s = searchField.map(field => ` ${field} ILIKE '%${escapedSearch}%'`).join(" OR ");
}
if (s) {
searchQuery = isMemberFilter ? ` (${s}) AND ` : ` AND (${s}) `;
}
} }
// Sort // Sort

View File

@@ -13,7 +13,6 @@ import {
Pagination, Pagination,
Segmented, Segmented,
Select, Select,
Skeleton,
Table, Table,
TablePaginationConfig, TablePaginationConfig,
Tooltip, Tooltip,
@@ -77,7 +76,6 @@ import {
} from '@/shared/worklenz-analytics-events'; } from '@/shared/worklenz-analytics-events';
import { useMixpanelTracking } from '@/hooks/useMixpanelTracking'; import { useMixpanelTracking } from '@/hooks/useMixpanelTracking';
import ProjectGroupList from '@/components/project-list/project-group/project-group-list'; import ProjectGroupList from '@/components/project-list/project-group/project-group-list';
import { groupProjects } from '@/utils/project-group';
const createFilters = (items: { id: string; name: string }[]) => const createFilters = (items: { id: string; name: string }[]) =>
items.map(item => ({ text: item.name, value: item.id })) as ColumnFilterItem[]; items.map(item => ({ text: item.name, value: item.id })) as ColumnFilterItem[];
@@ -129,7 +127,8 @@ const ProjectList: React.FC = () => {
return params; return params;
} }
return params; // Return the previous params to maintain reference stability
return JSON.parse(lastQueryParamsRef.current || '{}');
}, [requestParams]); }, [requestParams]);
// Use the optimized query with better error handling and caching // Use the optimized query with better error handling and caching
@@ -148,6 +147,8 @@ const ProjectList: React.FC = () => {
skip: viewMode === ProjectViewType.GROUP, skip: viewMode === ProjectViewType.GROUP,
}); });
// Add performance monitoring // Add performance monitoring
const performanceRef = useRef<{ startTime: number | null }>({ startTime: null }); const performanceRef = useRef<{ startTime: number | null }>({ startTime: null });
@@ -156,8 +157,6 @@ const ProjectList: React.FC = () => {
if (loadingProjects && !performanceRef.current.startTime) { if (loadingProjects && !performanceRef.current.startTime) {
performanceRef.current.startTime = performance.now(); performanceRef.current.startTime = performance.now();
} else if (!loadingProjects && performanceRef.current.startTime) { } else if (!loadingProjects && performanceRef.current.startTime) {
const duration = performance.now() - performanceRef.current.startTime;
console.log(`Projects query completed in ${duration.toFixed(2)}ms`);
performanceRef.current.startTime = null; performanceRef.current.startTime = null;
} }
}, [loadingProjects]); }, [loadingProjects]);
@@ -165,8 +164,6 @@ const ProjectList: React.FC = () => {
// Optimized debounced search with better cleanup and performance // Optimized debounced search with better cleanup and performance
const debouncedSearch = useCallback( const debouncedSearch = useCallback(
debounce((searchTerm: string) => { debounce((searchTerm: string) => {
console.log('Executing debounced search:', searchTerm);
// Clear any error messages when starting a new search // Clear any error messages when starting a new search
setErrorMessage(null); setErrorMessage(null);
@@ -372,7 +369,6 @@ const ProjectList: React.FC = () => {
// Handle query errors // Handle query errors
useEffect(() => { useEffect(() => {
if (projectsError) { if (projectsError) {
console.error('Projects query error:', projectsError);
setErrorMessage('Failed to load projects. Please try again.'); setErrorMessage('Failed to load projects. Please try again.');
} else { } else {
setErrorMessage(null); setErrorMessage(null);
@@ -392,7 +388,6 @@ const ProjectList: React.FC = () => {
await dispatch(fetchGroupedProjects(groupedRequestParams)).unwrap(); await dispatch(fetchGroupedProjects(groupedRequestParams)).unwrap();
} }
} catch (error) { } catch (error) {
console.error('Error refreshing projects:', error);
setErrorMessage('Failed to refresh projects. Please try again.'); setErrorMessage('Failed to refresh projects. Please try again.');
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@@ -632,7 +627,7 @@ const ProjectList: React.FC = () => {
}, },
{ {
title: t('category'), title: t('category'),
dataIndex: 'category', dataIndex: 'category_name',
key: 'category_id', key: 'category_id',
filters: categoryFilters, filters: categoryFilters,
filteredValue: filteredInfo.category_id || filteredCategories || [], filteredValue: filteredInfo.category_id || filteredCategories || [],
@@ -647,7 +642,7 @@ const ProjectList: React.FC = () => {
dataIndex: 'status', dataIndex: 'status',
key: 'status_id', key: 'status_id',
filters: statusFilters, filters: statusFilters,
filteredValue: filteredInfo.status_id || [], filteredValue: filteredInfo.status_id || filteredStatuses || [],
filterMultiple: true, filterMultiple: true,
sorter: true, sorter: true,
}, },
@@ -785,10 +780,10 @@ const ProjectList: React.FC = () => {
// Sync search input value with Redux state // Sync search input value with Redux state
useEffect(() => { useEffect(() => {
const currentSearch = viewMode === ProjectViewType.LIST ? requestParams.search : groupedRequestParams.search; const currentSearch = viewMode === ProjectViewType.LIST ? requestParams.search : groupedRequestParams.search;
if (searchValue !== currentSearch) { if (searchValue !== (currentSearch || '')) {
setSearchValue(currentSearch || ''); setSearchValue(currentSearch || '');
} }
}, [requestParams.search, groupedRequestParams.search, viewMode, searchValue]); }, [requestParams.search, groupedRequestParams.search, viewMode]); // Remove searchValue from deps to prevent loops
// Optimize loading state management // Optimize loading state management
useEffect(() => { useEffect(() => {
@@ -854,49 +849,47 @@ const ProjectList: React.FC = () => {
} }
/> />
<Card className="project-card"> <Card className="project-card">
<Skeleton active loading={isLoading} className="mt-4 p-4"> {viewMode === ProjectViewType.LIST ? (
{viewMode === ProjectViewType.LIST ? ( <Table<IProjectViewModel>
<Table<IProjectViewModel> columns={tableColumns}
columns={tableColumns} dataSource={tableDataSource}
dataSource={tableDataSource} rowKey={record => record.id || ''}
rowKey={record => record.id || ''} loading={loadingProjects || isFetchingProjects}
loading={loadingProjects} size="small"
size="small" onChange={handleTableChange}
onChange={handleTableChange} pagination={paginationConfig}
pagination={paginationConfig} locale={{ emptyText: emptyContent }}
locale={{ emptyText: emptyContent }} onRow={record => ({
onRow={record => ({ onClick: () => navigateToProject(record.id, record.team_member_default_view),
onClick: () => navigateToProject(record.id, record.team_member_default_view), onMouseEnter: () => handleProjectHover(record.id),
onMouseEnter: () => handleProjectHover(record.id), })}
})} />
) : (
<div>
<ProjectGroupList
groups={transformedGroupedProjects}
navigate={navigate}
onProjectSelect={id => navigateToProject(id, undefined)}
onArchive={() => {}}
isOwnerOrAdmin={isOwnerOrAdmin}
loading={groupedProjects.loading}
t={t}
/> />
) : ( {!groupedProjects.loading &&
<div> groupedProjects.data?.data &&
<ProjectGroupList groupedProjects.data.data.length > 0 && (
groups={transformedGroupedProjects} <div style={{ marginTop: '24px', textAlign: 'center' }}>
navigate={navigate} <Pagination
onProjectSelect={id => navigateToProject(id, undefined)} {...groupedPaginationConfig}
onArchive={() => {}} onChange={(page, pageSize) =>
isOwnerOrAdmin={isOwnerOrAdmin} handleGroupedTableChange({ current: page, pageSize })
loading={groupedProjects.loading} }
t={t} showTotal={paginationShowTotal}
/> />
{!groupedProjects.loading && </div>
groupedProjects.data?.data && )}
groupedProjects.data.data.length > 0 && ( </div>
<div style={{ marginTop: '24px', textAlign: 'center' }}> )}
<Pagination
{...groupedPaginationConfig}
onChange={(page, pageSize) =>
handleGroupedTableChange({ current: page, pageSize })
}
showTotal={paginationShowTotal}
/>
</div>
)}
</div>
)}
</Skeleton>
</Card> </Card>
{createPortal(<ProjectDrawer onClose={handleDrawerClose} />, document.body, 'project-drawer')} {createPortal(<ProjectDrawer onClose={handleDrawerClose} />, document.body, 'project-drawer')}