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:
@@ -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
|
||||||
|
|||||||
@@ -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,17 +157,13 @@ 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]);
|
||||||
|
|
||||||
// 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')}
|
||||||
|
|||||||
Reference in New Issue
Block a user