Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate SearchPage.js to function component #27924

Merged
merged 24 commits into from
Dec 20, 2023
Merged
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7aae936
Merge branch 'main' of https://github.com/Expensify/App into fix/1625…
Piotrfj Aug 23, 2023
cc5deed
Merge branch 'main' of https://github.com/Expensify/App into fix/1625…
Piotrfj Aug 31, 2023
86aa3a1
remove unnecessary search actions
Piotrfj Sep 21, 2023
6e31b01
Merge branch 'main' of https://github.com/Expensify/App into fix/1625…
Piotrfj Sep 21, 2023
1b81d14
Merge branch 'main' of https://github.com/Expensify/App into fix/1625…
Piotrfj Oct 26, 2023
3a20b60
fixes after conflicts
Piotrfj Oct 26, 2023
e212f25
fix lint error
Piotrfj Oct 26, 2023
0565cd3
Merge branch 'main' of https://github.com/Expensify/App into fix/1625…
Piotrfj Oct 30, 2023
0cd0edc
fix lint error
Piotrfj Oct 30, 2023
0b0359e
fix lint error
Piotrfj Nov 2, 2023
1b9d837
Merge branch 'main' of https://github.com/Expensify/App into fix/1625…
Piotrfj Nov 6, 2023
1df2c02
prettier
Piotrfj Nov 7, 2023
61228de
Merge branch 'main' of https://github.com/Expensify/App into fix/1625…
Piotrfj Nov 27, 2023
8d14ca8
refactor theme to functional way
Piotrfj Nov 27, 2023
ee5369e
prettier
Piotrfj Nov 28, 2023
c6f6911
Merge branch 'main' of https://github.com/Expensify/App into fix/1625…
Piotrfj Dec 6, 2023
8ad959a
bring back accidentally deleted code
Piotrfj Dec 8, 2023
8cc6813
Merge branch 'main' into fix/16251-SearchPage-refactoring
koko57 Dec 13, 2023
9fd4f3a
Merge branch 'main' into fix/16251-SearchPage-refactoring
koko57 Dec 15, 2023
8d329e8
Merge branch 'main' into fix/16251-SearchPage-refactoring
koko57 Dec 15, 2023
443a9ca
fix: apply requested changes
koko57 Dec 15, 2023
ad4cea8
fix: restore latest changes to SearchPage
koko57 Dec 18, 2023
0bbf68e
fix: resolve conflicts
koko57 Dec 20, 2023
253ccdd
fix: revert changes applied after another commit
koko57 Dec 20, 2023
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
273 changes: 127 additions & 146 deletions src/pages/SearchPage.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
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 Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import Performance from '@libs/Performance';
import * as ReportUtils from '@libs/ReportUtils';
import useThemeStyles from '@styles/useThemeStyles';
import * as Report from '@userActions/Report';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
Expand All @@ -35,209 +32,193 @@ 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}) {
// Data for initialization (runs only on the first render)
const {
recentReports: initialRecentReports,
personalDetails: initialPersonalDetails,
userToInvite: initialUserToInvite,
// Ignoring the rule because in this case we need the data only initially
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
// eslint-disable-next-line react-hooks/exhaustive-deps
} = useMemo(() => OptionsListUtils.getSearchOptions(reports, personalDetails, '', betas), []);

const [searchValue, setSearchValue] = useState('');
const [searchOptions, setSearchOptions] = useState({
recentReports: initialRecentReports,
personalDetails: initialPersonalDetails,
userToInvite: initialUserToInvite,
});

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: {},
};
}

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.
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
// 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is causing the previously stored searchValue to be sent and not the newest value

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);
},
);
setSearchValue('');
Navigation.dismissModal(option.reportID);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add this as a callback to to the setSearchValue like we did previously.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Piotrfj Can you please address this comment?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allroundexperts we cannot do it the same way we did in the class component. And I don't really see the necessity to do such here. What's more: the component always unmounts when we navigate to the report page, so the next time we open search page the search value is an empty string - no need to clear the input then.

} 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')}` : ''
}
shouldShowReferralCTA
referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these being removed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@allroundexperts- @Piotrfj already adressed this comment bring back accidentally deleted code

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()}
value={searchValue}
onSelectRow={selectReport}
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);
Loading