-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Migrate SearchPage.js to function component #27924
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
Changes from all commits
7aae936
cc5deed
86aa3a1
6e31b01
1b81d14
3a20b60
e212f25
0565cd3
0cd0edc
0b0359e
1b9d837
1df2c02
61228de
8d14ca8
ee5369e
c6f6911
8ad959a
8cc6813
9fd4f3a
8d329e8
443a9ca
ad4cea8
0bbf68e
253ccdd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||
|
|
@@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is causing the previously stored |
||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add this as a callback to to the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Piotrfj Can you please address this comment?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')}` : '' | ||
| } | ||
| 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); | ||
Uh oh!
There was an error while loading. Please reload this page.