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
33 changes: 30 additions & 3 deletions src/components/Reports/ReportTableSearchPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import debounce from 'lodash/debounce';
import { SEARCH } from '../../languages/en/ui';
import styles from './reportsPage.module.css';

const SEARCH_DEBOUNCE_MS = 300;

/**
* The search panel stateless component for Report grid
*/
function ReportTableSearchPanel({ onSearch, wildCardSearchText, onSearchClick }) {
const darkMode = useSelector(state => state.theme.darkMode);

// Keep the input responsive to every keystroke while debouncing the (expensive)
// filter recompute that onSearch triggers upstream in ReportsPage.
const [localSearchText, setLocalSearchText] = useState(wildCardSearchText ?? '');
const onSearchRef = useRef(onSearch);
onSearchRef.current = onSearch;

const debouncedSearch = useRef(
debounce(value => onSearchRef.current(value), SEARCH_DEBOUNCE_MS, {
leading: true,
trailing: true,
}),
).current;

useEffect(() => () => debouncedSearch.cancel(), [debouncedSearch]);

// Stay in sync when the search text is reset externally (e.g. "Clear Filters").
useEffect(() => {
if (wildCardSearchText === '' && localSearchText !== '') {
setLocalSearchText('');
}
}, [wildCardSearchText]);

const handleSearchClick = () => {
// Call the parent's search click handler if provided
if (onSearchClick) {
Expand Down Expand Up @@ -81,9 +106,11 @@ function ReportTableSearchPanel({ onSearch, wildCardSearchText, onSearchClick })
aria-label="Search"
placeholder="Search Text"
id="team-profiles-wild-card-search"
value={wildCardSearchText}
value={localSearchText}
onChange={e => {
onSearch(e.target.value); // Use destructured onSearch directly
const { value } = e.target;
setLocalSearchText(value);
debouncedSearch(value);
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ describe('<ReportTableSearchPanel />', () => {

vi.runAllTimers();

expect(onSearchMock).toHaveBeenCalledTimes(5); // adjust this if debounce is implemented
// Debounce is leading+trailing: the first keystroke ('h') fires immediately,
// then rapid keystrokes collapse into a single trailing call with the final value.
expect(onSearchMock).toHaveBeenCalledTimes(2);
expect(onSearchMock).toHaveBeenCalledWith('hello');
vi.useRealTimers();
});
Expand Down
9 changes: 9 additions & 0 deletions src/components/Teams/Teams.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { connect } from 'react-redux';
import { Container } from 'reactstrap';
import { toast } from 'react-toastify';
import isEqual from 'lodash/isEqual';
import debounce from 'lodash/debounce';
import { searchWithAccent } from '../../utils/search';
import {
getAllUserTeams,
Expand Down Expand Up @@ -61,13 +62,21 @@ class Teams extends React.PureComponent {
membersFetching: false,
selectedTeamMembers: [],
};
this.onWildCardSearch = debounce(this.onWildCardSearch, 300, {
leading: true,
trailing: true,
});
}

componentDidMount() {
this.props.getAllUserTeams(FILTER_ALL);
this.props.getAllUserProfile();
}

componentWillUnmount() {
this.onWildCardSearch.cancel();
}

componentDidUpdate(prevProps, prevState) {
const prevSlice = prevProps.state?.allTeamsData;
const currSlice = this.props.state?.allTeamsData;
Expand Down
26 changes: 18 additions & 8 deletions src/components/UserManagement/UserManagement.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import React from 'react';
import PropTypes from 'prop-types';
import debounce from 'lodash/debounce';
import { connect } from 'react-redux';
import { Container, Spinner } from 'reactstrap';
import { Table } from 'react-bootstrap';
Expand Down Expand Up @@ -55,6 +56,7 @@ class UserManagement extends React.PureComponent {
weeklyHrsSearchText: '',
emailSearchText: '',
wildCardSearchText: '',
rawSearchText: '',
selectedPage: props.state.userPagination.pagestats.selectedPage,
pageSize: props.state.userPagination.pagestats.pageSize,
allSelected: undefined,
Expand Down Expand Up @@ -86,6 +88,14 @@ class UserManagement extends React.PureComponent {
this.onDeleteButtonClick = this.onDeleteButtonClick.bind(this);
this.onFinalDayClick = this.onFinalDayClick.bind(this);
this.onActiveInactiveClick = this.onActiveInactiveClick.bind(this);
// Debounce committing the wildcard search text to state, since that state
// drives the (expensive) full user-list filter/re-render in componentDidUpdate.
// The input itself stays responsive via rawSearchText, updated on every keystroke.
this.debouncedApplyWildCardSearch = debounce(
searchText => this.setState({ wildCardSearchText: searchText, selectedPage: 1 }),
300,
{ leading: true, trailing: true },
);
}

componentDidMount() {
Expand All @@ -111,6 +121,7 @@ class UserManagement extends React.PureComponent {
componentWillUnmount() {
document.body.classList.remove('no-global-theme');
window.removeEventListener('resize', this.handleResize);
this.debouncedApplyWildCardSearch.cancel();
}

handleResize = () => {
Expand Down Expand Up @@ -644,13 +655,12 @@ class UserManagement extends React.PureComponent {
};

onWildCardSearch = (searchText) => {
this.setState(
{
wildCardSearchText: searchText,
selectedPage: 1,
},
() => this.updateGetFilteredData(),
);
// Update the visible input immediately; debounce the state change that
// actually re-filters/re-renders the (potentially large) user list.
// componentDidUpdate already re-runs getFilteredData when wildCardSearchText
// changes, so the debounced update below doesn't need its own explicit call.
this.setState({ rawSearchText: searchText });
this.debouncedApplyWildCardSearch(searchText);
};

onActiveFilter = (value) => {
Expand Down Expand Up @@ -794,7 +804,7 @@ class UserManagement extends React.PureComponent {
<>
<UserSearchPanel
onSearch={this.onWildCardSearch}
searchText={this.state.wildCardSearchText}
searchText={this.state.rawSearchText}
onActiveFilter={this.onActiveFilter}
onNewUserClick={this.onNewUserClick}
handleNewUserSetupPopup={this.handleNewUserSetupPopup}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { Dropdown, Input } from 'reactstrap';
import debounce from 'lodash/debounce';
import './TeamsAndProjects.module.css';
import { useSelector } from 'react-redux';
import appStyles from '~/App.module.css';

const SEARCH_DEBOUNCE_MS = 300;

// eslint-disable-next-line react/display-name
const AddProjectsAutoComplete = React.memo(props => {
const [isOpen, toggle] = useState(false);
Expand All @@ -18,6 +21,22 @@ const AddProjectsAutoComplete = React.memo(props => {
}
}, [props.selectedProject]);

// The input stays fully responsive via props.searchText; only the (re)filtering
// of the suggestion list is debounced so rapid keystrokes don't recompute it every time.
const [debouncedSearchText, setDebouncedSearchText] = useState(props.searchText);
const debouncedSetSearchText = useRef(
debounce(value => setDebouncedSearchText(value), SEARCH_DEBOUNCE_MS, {
leading: true,
trailing: true,
}),
).current;

useEffect(() => {
debouncedSetSearchText(props.searchText);
}, [props.searchText, debouncedSetSearchText]);

useEffect(() => () => debouncedSetSearchText.cancel(), [debouncedSetSearchText]);

return (
<Dropdown
isOpen={isOpen}
Expand Down Expand Up @@ -52,8 +71,7 @@ const AddProjectsAutoComplete = React.memo(props => {
{props.projectsData
.filter(project => {
if (
//prettier-ignore
props.formatText(project.projectName).indexOf(props.formatText(props.searchText)) >-1
props.formatText(project.projectName).includes(props.formatText(debouncedSearchText))
) {
return project;
}
Expand All @@ -75,7 +93,7 @@ const AddProjectsAutoComplete = React.memo(props => {
))}

{props.projectsData.every(
item => props.formatText(item.projectName) !== props.formatText(props.searchText),
item => props.formatText(item.projectName) !== props.formatText(debouncedSearchText),
) && (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
Expand All @@ -85,7 +103,7 @@ const AddProjectsAutoComplete = React.memo(props => {
props.setIsOpenDropdown(true);
}}
>
Create new project: {props.searchText}
Create new project: {debouncedSearchText}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, { useRef, useEffect } from 'react';
import React, { useRef, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Dropdown, Input } from 'reactstrap';
import debounce from 'lodash/debounce';
import './TeamsAndProjects.module.css';
import { useSelector } from 'react-redux';
import appStyles from '~/App.module.css';

const TEAM_NAME_MAX_LENGTH = 100;
const SEARCH_DEBOUNCE_MS = 300;

// eslint-disable-next-line react/display-name
const AddTeamsAutoComplete = React.memo((props) => {
Expand All @@ -27,11 +29,27 @@ const AddTeamsAutoComplete = React.memo((props) => {
const normalize = (s) =>
(s ?? '').toString().toLowerCase().trim().replace(/\s+/g, ' ');

// The input stays fully responsive via searchText; only the (re)filtering of the
// suggestion list is debounced so rapid keystrokes don't recompute it every time.
const [debouncedSearchText, setDebouncedSearchText] = useState(searchText);
const debouncedSetSearchText = useRef(
debounce((value) => setDebouncedSearchText(value), SEARCH_DEBOUNCE_MS, {
leading: true,
trailing: true,
}),
).current;

useEffect(() => {
debouncedSetSearchText(searchText);
}, [searchText, debouncedSetSearchText]);

useEffect(() => () => debouncedSetSearchText.cancel(), [debouncedSetSearchText]);

const suggestions = React.useMemo(() => {
const q = normalize(searchText);
const q = normalize(debouncedSearchText);
if (!q) return allTeams; // show all when empty
return allTeams.filter((t) => normalize(t.teamName).includes(q));
}, [allTeams, searchText]);
}, [allTeams, debouncedSearchText]);

const handlePick = (team) => {
onDropDownSelect(team);
Expand Down
Loading