-
Notifications
You must be signed in to change notification settings - Fork 3k
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 MoneyRequestParticipantsSplitSelector.js to function component #20271
Changes from 13 commits
f8661e7
8ecd4fb
0b66de4
e10cd88
35b3f79
4c375f9
041d782
c068bfb
4dd5f0a
ffd6fad
49252c1
82be71e
a1e5ba1
05a9937
0afb3e6
b70b37e
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,4 +1,4 @@ | ||
import React, {Component} from 'react'; | ||
import React, {useCallback, useEffect, useMemo, useState} from 'react'; | ||
import {View} from 'react-native'; | ||
import PropTypes from 'prop-types'; | ||
import _ from 'underscore'; | ||
|
@@ -60,183 +60,158 @@ const defaultProps = { | |
safeAreaPaddingBottomStyle: {}, | ||
}; | ||
|
||
class MoneyRequestParticipantsSplitSelector extends Component { | ||
constructor(props) { | ||
super(props); | ||
function MoneyRequestParticipantsSplitSelector(props) { | ||
const [searchTerm, setSearchTerm] = useState(''); | ||
const [newChatOptions, setNewChatOptions] = useState({ | ||
recentReports: [], | ||
personalDetails: [], | ||
userToInvite: null, | ||
}); | ||
|
||
this.toggleOption = this.toggleOption.bind(this); | ||
this.finalizeParticipants = this.finalizeParticipants.bind(this); | ||
this.updateOptionsWithSearchTerm = this.updateOptionsWithSearchTerm.bind(this); | ||
|
||
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions( | ||
props.reports, | ||
props.personalDetails, | ||
props.betas, | ||
'', | ||
props.participants, | ||
CONST.EXPENSIFY_EMAILS, | ||
); | ||
|
||
this.state = { | ||
searchTerm: '', | ||
recentReports, | ||
personalDetails, | ||
userToInvite, | ||
}; | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { | ||
return; | ||
} | ||
this.updateOptionsWithSearchTerm(this.state.searchTerm); | ||
} | ||
const maxParticipantsReached = props.participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; | ||
|
||
/** | ||
* Returns the sections needed for the OptionsSelector | ||
* | ||
* @param {Boolean} maxParticipantsReached | ||
* @returns {Array} | ||
*/ | ||
getSections(maxParticipantsReached) { | ||
const sections = []; | ||
const sections = useMemo(() => { | ||
const newSections = []; | ||
let indexOffset = 0; | ||
|
||
sections.push({ | ||
newSections.push({ | ||
title: undefined, | ||
data: this.props.participants, | ||
data: props.participants, | ||
shouldShow: true, | ||
indexOffset, | ||
}); | ||
indexOffset += this.props.participants.length; | ||
indexOffset += props.participants.length; | ||
|
||
if (maxParticipantsReached) { | ||
return sections; | ||
return newSections; | ||
} | ||
|
||
sections.push({ | ||
title: this.props.translate('common.recents'), | ||
data: this.state.recentReports, | ||
shouldShow: !_.isEmpty(this.state.recentReports), | ||
const {recentReports, personalDetails, userToInvite} = newChatOptions; | ||
|
||
newSections.push({ | ||
title: props.translate('common.recents'), | ||
data: recentReports, | ||
shouldShow: !_.isEmpty(recentReports), | ||
indexOffset, | ||
}); | ||
indexOffset += this.state.recentReports.length; | ||
indexOffset += recentReports.length; | ||
|
||
sections.push({ | ||
title: this.props.translate('common.contacts'), | ||
data: this.state.personalDetails, | ||
shouldShow: !_.isEmpty(this.state.personalDetails), | ||
newSections.push({ | ||
title: props.translate('common.contacts'), | ||
data: personalDetails, | ||
shouldShow: !_.isEmpty(personalDetails), | ||
indexOffset, | ||
}); | ||
indexOffset += this.state.personalDetails.length; | ||
indexOffset += personalDetails.length; | ||
|
||
if (this.state.userToInvite && !OptionsListUtils.isCurrentUser(this.state.userToInvite)) { | ||
sections.push({ | ||
if (userToInvite && !OptionsListUtils.isCurrentUser(userToInvite)) { | ||
newSections.push({ | ||
undefined, | ||
data: [this.state.userToInvite], | ||
data: [userToInvite], | ||
shouldShow: true, | ||
indexOffset, | ||
}); | ||
} | ||
|
||
return sections; | ||
} | ||
|
||
updateOptionsWithSearchTerm(searchTerm = '') { | ||
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions( | ||
this.props.reports, | ||
this.props.personalDetails, | ||
this.props.betas, | ||
searchTerm, | ||
this.props.participants, | ||
CONST.EXPENSIFY_EMAILS, | ||
); | ||
this.setState({ | ||
searchTerm, | ||
userToInvite, | ||
recentReports, | ||
personalDetails, | ||
}); | ||
} | ||
|
||
/** | ||
* Once a single or more users are selected, navigates to next step | ||
*/ | ||
finalizeParticipants() { | ||
this.props.onStepComplete(); | ||
} | ||
return newSections; | ||
// eslint-disable-next-line react-hooks/exhaustive-deps -- props does not need to be a dependency as it will always exist | ||
}, [maxParticipantsReached, newChatOptions, props.participants, props.translate]); | ||
|
||
/** | ||
* Removes a selected option from list if already selected. If not already selected add this option to the list. | ||
* @param {Object} option | ||
*/ | ||
toggleOption(option) { | ||
const isOptionInList = _.some(this.props.participants, (selectedOption) => selectedOption.accountID === option.accountID); | ||
const toggleOption = useCallback( | ||
(option) => { | ||
const isOptionInList = _.some(props.participants, (selectedOption) => selectedOption.accountID === option.accountID); | ||
|
||
let newSelectedOptions; | ||
let newSelectedOptions; | ||
|
||
if (isOptionInList) { | ||
newSelectedOptions = _.reject(this.props.participants, (selectedOption) => selectedOption.accountID === option.accountID); | ||
} else { | ||
newSelectedOptions = [...this.props.participants, option]; | ||
} | ||
if (isOptionInList) { | ||
newSelectedOptions = _.reject(props.participants, (selectedOption) => selectedOption.accountID === option.accountID); | ||
} else { | ||
newSelectedOptions = [...props.participants, option]; | ||
} | ||
|
||
this.props.onAddParticipants(newSelectedOptions); | ||
props.onAddParticipants(newSelectedOptions); | ||
|
||
this.setState((prevState) => { | ||
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions( | ||
this.props.reports, | ||
this.props.personalDetails, | ||
this.props.betas, | ||
isOptionInList ? prevState.searchTerm : '', | ||
props.reports, | ||
props.personalDetails, | ||
props.betas, | ||
isOptionInList ? searchTerm : '', | ||
newSelectedOptions, | ||
CONST.EXPENSIFY_EMAILS, | ||
); | ||
return { | ||
|
||
setNewChatOptions({ | ||
recentReports, | ||
personalDetails, | ||
userToInvite, | ||
searchTerm: isOptionInList ? prevState.searchTerm : '', | ||
}; | ||
}); | ||
} | ||
|
||
render() { | ||
const maxParticipantsReached = this.props.participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; | ||
const sections = this.getSections(maxParticipantsReached); | ||
const headerMessage = OptionsListUtils.getHeaderMessage( | ||
this.state.personalDetails.length + this.state.recentReports.length !== 0, | ||
Boolean(this.state.userToInvite), | ||
this.state.searchTerm, | ||
maxParticipantsReached, | ||
); | ||
const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); | ||
|
||
return ( | ||
<View style={[styles.flex1, styles.w100, this.props.participants.length > 0 ? this.props.safeAreaPaddingBottomStyle : {}]}> | ||
<OptionsSelector | ||
canSelectMultipleOptions | ||
sections={sections} | ||
selectedOptions={this.props.participants} | ||
value={this.state.searchTerm} | ||
onSelectRow={this.toggleOption} | ||
onChangeText={this.updateOptionsWithSearchTerm} | ||
headerMessage={headerMessage} | ||
boldStyle | ||
shouldShowConfirmButton | ||
confirmButtonText={this.props.translate('common.next')} | ||
onConfirmSelection={this.finalizeParticipants} | ||
textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')} | ||
safeAreaPaddingBottomStyle={this.props.safeAreaPaddingBottomStyle} | ||
shouldShowOptions={isOptionsDataReady} | ||
/> | ||
</View> | ||
}); | ||
if (!isOptionInList) { | ||
setSearchTerm(''); | ||
} | ||
}, | ||
// eslint-disable-next-line react-hooks/exhaustive-deps -- props does not need to be a dependency as it will always exist | ||
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. Same question 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. I got a warning when I checked lint. So I checked the useMemo and useCallback functions in other components and I noticed they did same comment. That's why I disabled it. |
||
[searchTerm, props.participants, props.onAddParticipants, props.reports, props.personalDetails, props.betas, setNewChatOptions, setSearchTerm], | ||
); | ||
|
||
const headerMessage = OptionsListUtils.getHeaderMessage( | ||
newChatOptions.personalDetails.length + newChatOptions.recentReports.length !== 0, | ||
Boolean(newChatOptions.userToInvite), | ||
searchTerm, | ||
maxParticipantsReached, | ||
); | ||
const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(props.personalDetails); | ||
|
||
useEffect(() => { | ||
const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getNewChatOptions( | ||
props.reports, | ||
props.personalDetails, | ||
props.betas, | ||
searchTerm, | ||
props.participants, | ||
CONST.EXPENSIFY_EMAILS, | ||
); | ||
} | ||
setNewChatOptions({ | ||
recentReports, | ||
personalDetails, | ||
userToInvite, | ||
}); | ||
}, [props.betas, props.reports, props.participants, props.personalDetails, props.translate, searchTerm]); | ||
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. I don't see the original implementation having such thing. Can you please explain why this is needed? 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. You can see the 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. Which line exactly are you referring to @allroundexperts? If it's 181 that makes sense to me as we'll trigger updates based on those changes. But let me know if you mean somethign else. 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. I was talking about the whole 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.
These are original implementation. We can change these functions using
Like above. And in the original component, whenever change Actually, in the original implementation, main functional block was FYI, I thought it is not good to reuse the callback function in the component. |
||
|
||
return ( | ||
<View style={[styles.flex1, styles.w100, props.participants.length > 0 ? props.safeAreaPaddingBottomStyle : {}]}> | ||
<OptionsSelector | ||
canSelectMultipleOptions | ||
sections={sections} | ||
selectedOptions={props.participants} | ||
value={searchTerm} | ||
onSelectRow={toggleOption} | ||
onChangeText={setSearchTerm} | ||
headerMessage={headerMessage} | ||
boldStyle | ||
shouldShowConfirmButton | ||
confirmButtonText={props.translate('common.next')} | ||
onConfirmSelection={props.onStepComplete} | ||
textInputLabel={props.translate('optionsSelector.nameEmailOrPhoneNumber')} | ||
safeAreaPaddingBottomStyle={props.safeAreaPaddingBottomStyle} | ||
shouldShowOptions={isOptionsDataReady} | ||
/> | ||
</View> | ||
); | ||
} | ||
|
||
MoneyRequestParticipantsSplitSelector.propTypes = propTypes; | ||
MoneyRequestParticipantsSplitSelector.defaultProps = defaultProps; | ||
MoneyRequestParticipantsSplitSelector.displayName = 'MoneyRequestParticipantsSplitSelector'; | ||
|
||
export default compose( | ||
withLocalize, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you disabling this check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we're no longer disabling the check, but I think it dows make sense, right? We shouldn't update after ANY prop is changed, just specific ones?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's correct @Julesssss. We've added into dependency all the properties that should trigger a re-calculation of values in the function.