Skip to content

Commit

Permalink
Merge pull request #27924 from Piotrfj/fix/16251-SearchPage-refactoring
Browse files Browse the repository at this point in the history
Migrate SearchPage.js to function component
  • Loading branch information
marcaaron authored Dec 20, 2023
2 parents e76c75d + 253ccdd commit 8669b24
Showing 1 changed file with 128 additions and 143 deletions.
271 changes: 128 additions & 143 deletions src/pages/SearchPage.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import PropTypes from 'prop-types';
import React, {Component} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import networkPropTypes from '@components/networkPropTypes';
import {withNetwork} from '@components/OnyxProvider';
import OptionsSelector from '@components/OptionsSelector';
import ScreenWrapper from '@components/ScreenWrapper';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
import compose from '@libs/compose';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import Performance from '@libs/Performance';
Expand All @@ -35,207 +32,195 @@ const propTypes = {
/** All reports shared with the user */
reports: PropTypes.objectOf(reportPropTypes),

/** Window Dimensions Props */
...windowDimensionsPropTypes,

...withLocalizePropTypes,

/** Network info */
network: networkPropTypes,

/** Whether we are searching for reports in the server */
isSearchingForReports: PropTypes.bool,
...withThemeStylesPropTypes,
};

const defaultProps = {
betas: [],
personalDetails: {},
reports: {},
network: {},
isSearchingForReports: false,
};

class SearchPage extends Component {
constructor(props) {
super(props);
function SearchPage({betas, personalDetails, reports, isSearchingForReports}) {
const [searchValue, setSearchValue] = useState('');
const [searchOptions, setSearchOptions] = useState(() => {
const {
recentReports: localRecentReports,
personalDetails: localPersonalDetails,
userToInvite: localUserToInvite,
} = OptionsListUtils.getSearchOptions(reports, personalDetails, '', betas);

return {
recentReports: localRecentReports,
personalDetails: localPersonalDetails,
userToInvite: localUserToInvite,
};
});

const {isOffline} = useNetwork();
const {translate} = useLocalize();
const themeStyles = useThemeStyles();
const isMounted = useRef(false);

const updateOptions = useCallback(() => {
const {
recentReports: localRecentReports,
personalDetails: localPersonalDetails,
userToInvite: localUserToInvite,
} = OptionsListUtils.getSearchOptions(reports, personalDetails, searchValue.trim(), betas);

setSearchOptions({
recentReports: localRecentReports,
personalDetails: localPersonalDetails,
userToInvite: localUserToInvite,
});
}, [reports, personalDetails, searchValue, betas]);

const debouncedUpdateOptions = useMemo(() => _.debounce(updateOptions, 75), [updateOptions]);

useEffect(() => {
Timing.start(CONST.TIMING.SEARCH_RENDER);
Performance.markStart(CONST.TIMING.SEARCH_RENDER);
}, []);

this.searchRendered = this.searchRendered.bind(this);
this.selectReport = this.selectReport.bind(this);
this.onChangeText = this.onChangeText.bind(this);
this.updateOptions = this.updateOptions.bind(this);
this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75);
this.state = {
searchValue: '',
recentReports: {},
personalDetails: {},
userToInvite: {},
};
}
useEffect(() => {
updateOptions();
}, [reports, personalDetails, betas, updateOptions]);

componentDidUpdate(prevProps) {
if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) {
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true;
return;
}
this.updateOptions();
}

onChangeText(searchValue = '') {
Report.searchInServer(searchValue);
this.setState({searchValue}, this.debouncedUpdateOptions);
}
debouncedUpdateOptions();
// Ignoring the rule intentionally, we want to run the code only when search Value changes to prevent additional runs.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchValue]);

/**
* Returns the sections needed for the OptionsSelector
*
* @returns {Array}
*/
getSections() {
const getSections = () => {
const sections = [];
let indexOffset = 0;

if (this.state.recentReports.length > 0) {
if (searchOptions.recentReports.length > 0) {
sections.push({
data: this.state.recentReports,
data: searchOptions.recentReports,
shouldShow: true,
indexOffset,
});
indexOffset += this.state.recentReports.length;
indexOffset += searchOptions.recentReports.length;
}

if (this.state.personalDetails.length > 0) {
if (searchOptions.personalDetails.length > 0) {
sections.push({
data: this.state.personalDetails,
data: searchOptions.personalDetails,
shouldShow: true,
indexOffset,
});
indexOffset += this.state.recentReports.length;
indexOffset += searchOptions.recentReports.length;
}

if (this.state.userToInvite) {
if (searchOptions.userToInvite) {
sections.push({
data: [this.state.userToInvite],
data: [searchOptions.userToInvite],
shouldShow: true,
indexOffset,
});
}

return sections;
}
};

searchRendered() {
const searchRendered = () => {
Timing.end(CONST.TIMING.SEARCH_RENDER);
Performance.markEnd(CONST.TIMING.SEARCH_RENDER);
}

updateOptions() {
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions(
this.props.reports,
this.props.personalDetails,
this.state.searchValue.trim(),
this.props.betas,
);
this.setState({
userToInvite,
recentReports,
personalDetails,
});
}
};

const onChangeText = (value = '') => {
Report.searchInServer(searchValue);
setSearchValue(value);
};

/**
* Reset the search value and redirect to the selected report
*
* @param {Object} option
*/
selectReport(option) {
const selectReport = (option) => {
if (!option) {
return;
}

if (option.reportID) {
this.setState(
{
searchValue: '',
},
() => {
Navigation.dismissModal(option.reportID);
},
);
Navigation.dismissModal(option.reportID);
} else {
Report.navigateToAndOpenReport([option.login]);
}
}

render() {
const sections = this.getSections();
const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails);
const headerMessage = OptionsListUtils.getHeaderMessage(
this.state.recentReports.length + this.state.personalDetails.length !== 0,
Boolean(this.state.userToInvite),
this.state.searchValue,
);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={SearchPage.displayName}
onEntryTransitionEnd={this.updateOptions}
>
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
<>
<HeaderWithBackButton title={this.props.translate('common.search')} />
<View style={[this.props.themeStyles.flex1, this.props.themeStyles.w100, this.props.themeStyles.pRelative]}>
<OptionsSelector
sections={sections}
value={this.state.searchValue}
onSelectRow={this.selectReport}
onChangeText={this.onChangeText}
headerMessage={headerMessage}
hideSectionHeaders
showTitleTooltip
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')}
textInputAlert={
this.props.network.isOffline ? `${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}` : ''
}
onLayout={this.searchRendered}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
autoFocus
isLoadingNewOptions={this.props.isSearchingForReports}
/>
</View>
</>
)}
</ScreenWrapper>
);
}
};

const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails);
const headerMessage = OptionsListUtils.getHeaderMessage(
searchOptions.recentReports.length + searchOptions.personalDetails.length !== 0,
Boolean(searchOptions.userToInvite),
searchValue,
);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={SearchPage.displayName}
onEntryTransitionEnd={updateOptions}
>
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
<>
<HeaderWithBackButton title={translate('common.search')} />
<View style={[themeStyles.flex1, themeStyles.w100, themeStyles.pRelative]}>
<OptionsSelector
sections={getSections()}
onSelectRow={selectReport}
value={searchValue}
onChangeText={onChangeText}
headerMessage={headerMessage}
hideSectionHeaders
showTitleTooltip
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
shouldShowReferralCTA
referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}
textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''}
onLayout={searchRendered}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
autoFocus
isLoadingNewOptions={isSearchingForReports}
/>
</View>
</>
)}
</ScreenWrapper>
);
}

SearchPage.propTypes = propTypes;
SearchPage.defaultProps = defaultProps;
SearchPage.displayName = 'SearchPage';

export default compose(
withLocalize,
withWindowDimensions,
withNetwork(),
withOnyx({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
betas: {
key: ONYXKEYS.BETAS,
},
isSearchingForReports: {
key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
initWithStoredValues: false,
},
}),
withThemeStyles,
)(SearchPage);
export default withOnyx({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
betas: {
key: ONYXKEYS.BETAS,
},
isSearchingForReports: {
key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
initWithStoredValues: false,
},
})(SearchPage);

0 comments on commit 8669b24

Please sign in to comment.