Skip to content

fix/14490: No data displayed in options list if component using report data is rendered too early #17349

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

Merged
merged 16 commits into from
May 10, 2023
Merged
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
63 changes: 35 additions & 28 deletions src/components/OptionsList/BaseOptionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import OptionRow from '../OptionRow';
import SectionList from '../SectionList';
import Text from '../Text';
import {propTypes as optionsListPropTypes, defaultProps as optionsListDefaultProps} from './optionsListPropTypes';
import OptionsListSkeletonView from '../OptionsListSkeletonView';

const propTypes = {
/** Determines whether the keyboard gets dismissed in response to a drag */
Expand Down Expand Up @@ -49,7 +50,7 @@ class BaseOptionsList extends Component {
nextProps.focusedIndex !== this.props.focusedIndex ||
nextProps.selectedOptions.length !== this.props.selectedOptions.length ||
nextProps.headerMessage !== this.props.headerMessage ||
!_.isEqual(nextProps.sections, this.props.sections) ||
nextProps.isLoading !== this.props.isLoading ||
!_.isEqual(nextProps.sections, this.props.sections)
);
}
Expand Down Expand Up @@ -206,33 +207,39 @@ class BaseOptionsList extends Component {
render() {
return (
<View style={this.props.listContainerStyles}>
{this.props.headerMessage ? (
<View style={[styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{this.props.headerMessage}</Text>
</View>
) : null}
<SectionList
ref={this.props.innerRef}
indicatorStyle="white"
keyboardShouldPersistTaps="always"
keyboardDismissMode={this.props.keyboardDismissMode}
onScrollBeginDrag={this.props.onScrollBeginDrag}
onScroll={this.props.onScroll}
contentContainerStyle={this.props.contentContainerStyles}
showsVerticalScrollIndicator={false}
sections={this.props.sections}
keyExtractor={this.extractKey}
stickySectionHeadersEnabled={false}
renderItem={this.renderItem}
getItemLayout={this.getItemLayout}
renderSectionHeader={this.renderSectionHeader}
extraData={this.props.focusedIndex}
initialNumToRender={12}
maxToRenderPerBatch={5}
windowSize={5}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
onViewableItemsChanged={this.onViewableItemsChanged}
/>
{this.props.isLoading ? (
<OptionsListSkeletonView />
) : (
<>
{this.props.headerMessage ? (
<View style={[styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{this.props.headerMessage}</Text>
</View>
) : null}
<SectionList
ref={this.props.innerRef}
indicatorStyle="white"
keyboardShouldPersistTaps="always"
keyboardDismissMode={this.props.keyboardDismissMode}
onScrollBeginDrag={this.props.onScrollBeginDrag}
onScroll={this.props.onScroll}
contentContainerStyle={this.props.contentContainerStyles}
showsVerticalScrollIndicator={false}
sections={this.props.sections}
keyExtractor={this.extractKey}
stickySectionHeadersEnabled={false}
renderItem={this.renderItem}
getItemLayout={this.getItemLayout}
renderSectionHeader={this.renderSectionHeader}
extraData={this.props.focusedIndex}
initialNumToRender={12}
maxToRenderPerBatch={5}
windowSize={5}
viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
onViewableItemsChanged={this.onViewableItemsChanged}
/>
</>
)}
</View>
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/components/OptionsList/optionsListPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const propTypes = {
/** Whether to disable the interactivity of the list's option row(s) */
isDisabled: PropTypes.bool,

/** Whether the options list skeleton loading view should be displayed */
isLoading: PropTypes.bool,

/** Callback to execute when the SectionList lays out */
onLayout: PropTypes.func,

Expand All @@ -90,6 +93,7 @@ const defaultProps = {
innerRef: null,
showTitleTooltip: false,
isDisabled: false,
isLoading: false,
onLayout: undefined,
shouldHaveOptionSeparator: false,
shouldDisableRowInnerPadding: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const defaultTypes = {
shouldAnimate: true,
};

class LHNSkeletonView extends React.Component {
class OptionsListSkeletonView extends React.Component {
Copy link
Member

@rushatgabhane rushatgabhane May 25, 2023

Choose a reason for hiding this comment

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

Hi everyone! dropping a gentle note here. When we generalised this component for OptionsList, we forgot to hide the overflow of extra skeletons when there is a button at the bottom.

Causing this bug -

image

Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch! Thanks for the fix

constructor(props) {
super(props);
this.state = {
Expand Down Expand Up @@ -105,7 +105,7 @@ class LHNSkeletonView extends React.Component {
}
}

LHNSkeletonView.propTypes = propTypes;
LHNSkeletonView.defaultProps = defaultTypes;
OptionsListSkeletonView.propTypes = propTypes;
OptionsListSkeletonView.defaultProps = defaultTypes;

export default LHNSkeletonView;
export default OptionsListSkeletonView;
6 changes: 2 additions & 4 deletions src/components/OptionsSelector/BaseOptionsSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import TextInput from '../TextInput';
import ArrowKeyFocusManager from '../ArrowKeyFocusManager';
import KeyboardShortcut from '../../libs/KeyboardShortcut';
import FullScreenLoadingIndicator from '../FullscreenLoadingIndicator';
import {propTypes as optionsSelectorPropTypes, defaultProps as optionsSelectorDefaultProps} from './optionsSelectorPropTypes';
import setSelection from '../../libs/setSelection';

Expand Down Expand Up @@ -289,7 +288,7 @@ class BaseOptionsSelector extends Component {
blurOnSubmit={Boolean(this.state.allOptions.length)}
/>
);
const optionsList = this.props.shouldShowOptions ? (
const optionsList = (
<OptionsList
ref={(el) => (this.list = el)}
optionHoveredStyle={this.props.optionHoveredStyle}
Expand All @@ -314,9 +313,8 @@ class BaseOptionsSelector extends Component {
}
}}
contentContainerStyles={shouldShowFooter ? undefined : [this.props.safeAreaPaddingBottomStyle]}
isLoading={!this.props.shouldShowOptions}
/>
) : (
<FullScreenLoadingIndicator />
);
return (
<ArrowKeyFocusManager
Expand Down
19 changes: 18 additions & 1 deletion src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,15 @@ function getPersonalDetailsForLogins(logins, personalDetails) {
return personalDetailsForLogins;
}

/**
* Return true if personal details data is ready, i.e. report list options can be created.
* @param {Object} personalDetails
* @returns {boolean}
*/
function isPersonalDetailsReady(personalDetails) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't it better to define this function in PersonalDetailsUtils? It won't require personalDetails parameter as already connected to Onyx

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was also my first thought, but I ran into a dependency cycle issue when trying to import PersonalDetailsUtils inside OptionsListUtils which I'm not sure how to resolve.
image

return !_.isEmpty(personalDetails) && !_.some(_.keys(personalDetails), key => !personalDetails[key].login);
}

/**
* Get the participant options for a report.
* @param {Object} report
Expand Down Expand Up @@ -523,7 +532,6 @@ function getOptions(
includeMultipleParticipantReports = false,
includePersonalDetails = false,
includeRecentReports = false,

// When sortByReportTypeInSearch flag is true, recentReports will include the personalDetails options as well.
sortByReportTypeInSearch = false,
searchInputValue = '',
Expand All @@ -533,6 +541,14 @@ function getOptions(
includeOwnedWorkspaceChats = false,
},
) {
if (!isPersonalDetailsReady(personalDetails)) {
return {
recentReports: [],
personalDetails: [],
userToInvite: null,
};
}

let recentReportOptions = [];
let personalDetailsOptions = [];
const reportMapForLogins = {};
Expand Down Expand Up @@ -897,6 +913,7 @@ export {
addSMSDomainIfPhoneNumber,
getAvatarsForLogins,
isCurrentUser,
isPersonalDetailsReady,
getSearchOptions,
getNewChatOptions,
getShareDestinationOptions,
Expand Down
54 changes: 33 additions & 21 deletions src/pages/NewChatPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../components/wit
import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
import Navigation from '../libs/Navigation/Navigation';
import ScreenWrapper from '../components/ScreenWrapper';
import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import compose from '../libs/compose';
import personalDetailsPropType from './personalDetailsPropType';
Expand All @@ -32,6 +31,9 @@ const propTypes = {
/** All reports shared with the user */
reports: PropTypes.objectOf(reportPropTypes),

/** Indicates whether the reports data is ready */
isLoadingReportData: PropTypes.bool,

...windowDimensionsPropTypes,

...withLocalizePropTypes,
Expand All @@ -42,6 +44,7 @@ const defaultProps = {
betas: [],
personalDetails: {},
reports: {},
isLoadingReportData: true,
};

class NewChatPage extends Component {
Expand Down Expand Up @@ -71,6 +74,13 @@ class NewChatPage extends Component {
};
}

componentDidUpdate(prevProps) {
if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) {
return;
}
this.updateOptionsWithSearchTerm(this.state.searchTerm);
}

/**
* Returns the sections needed for the OptionsSelector
*
Expand Down Expand Up @@ -216,6 +226,8 @@ class NewChatPage extends Component {
this.state.searchTerm,
maxParticipantsReached,
);
const isOptionsDataReady = !this.props.isLoadingReportData && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
Expand All @@ -228,26 +240,23 @@ class NewChatPage extends Component {
onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
<View style={[styles.flex1, styles.w100, styles.pRelative, this.state.selectedOptions.length > 0 ? safeAreaPaddingBottomStyle : {}]}>
{didScreenTransitionEnd ? (
<OptionsSelector
canSelectMultipleOptions={this.props.isGroupChat}
sections={sections}
selectedOptions={this.state.selectedOptions}
value={this.state.searchTerm}
onSelectRow={(option) => (this.props.isGroupChat ? this.toggleOption(option) : this.createChat(option))}
onChangeText={this.updateOptionsWithSearchTerm}
headerMessage={headerMessage}
boldStyle
shouldFocusOnSelectRow={this.props.isGroupChat}
shouldShowConfirmButton={this.props.isGroupChat}
confirmButtonText={this.props.translate('newChatPage.createGroup')}
onConfirmSelection={this.createGroup}
textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
/>
) : (
<FullScreenLoadingIndicator />
)}
<OptionsSelector
canSelectMultipleOptions={this.props.isGroupChat}
sections={sections}
selectedOptions={this.state.selectedOptions}
value={this.state.searchTerm}
onSelectRow={(option) => (this.props.isGroupChat ? this.toggleOption(option) : this.createChat(option))}
onChangeText={this.updateOptionsWithSearchTerm}
headerMessage={headerMessage}
boldStyle
shouldFocusOnSelectRow={this.props.isGroupChat}
shouldShowConfirmButton={this.props.isGroupChat}
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
confirmButtonText={this.props.translate('newChatPage.createGroup')}
onConfirmSelection={this.createGroup}
textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
/>
</View>
</>
)}
Expand All @@ -272,5 +281,8 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
isLoadingReportData: {
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
},
}),
)(NewChatPage);
18 changes: 17 additions & 1 deletion src/pages/SearchPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const propTypes = {
/** All reports shared with the user */
reports: PropTypes.objectOf(reportPropTypes),

/** Indicates whether report data is ready */
isLoadingReportData: PropTypes.bool,

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

Expand All @@ -43,6 +46,7 @@ const defaultProps = {
betas: [],
personalDetails: {},
reports: {},
isLoadingReportData: true,
};

class SearchPage extends Component {
Expand All @@ -68,6 +72,13 @@ class SearchPage extends Component {
};
}

componentDidUpdate(prevProps) {
if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) {
return;
}
this.updateOptions();
}

onChangeText(searchValue = '') {
this.setState({searchValue}, this.debouncedUpdateOptions);
}
Expand Down Expand Up @@ -159,6 +170,8 @@ class SearchPage extends Component {

render() {
const sections = this.getSections();
const isOptionsDataReady = !this.props.isLoadingReportData && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails);

return (
<ScreenWrapper includeSafeAreaPaddingBottom={false}>
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
Expand All @@ -176,7 +189,7 @@ class SearchPage extends Component {
headerMessage={this.state.headerMessage}
hideSectionHeaders
showTitleTooltip
shouldShowOptions={didScreenTransitionEnd}
shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady}
textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')}
onLayout={this.searchRendered}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
Expand Down Expand Up @@ -205,5 +218,8 @@ export default compose(
betas: {
key: ONYXKEYS.BETAS,
},
isLoadingReportData: {
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
},
}),
)(SearchPage);
4 changes: 2 additions & 2 deletions src/pages/home/sidebar/SidebarLinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import LHNOptionsList from '../../../components/LHNOptionsList/LHNOptionsList';
import SidebarUtils from '../../../libs/SidebarUtils';
import reportPropTypes from '../../reportPropTypes';
import OfflineWithFeedback from '../../../components/OfflineWithFeedback';
import LHNSkeletonView from '../../../components/LHNSkeletonView';
import OptionsListSkeletonView from '../../../components/OptionsListSkeletonView';

const propTypes = {
/** Toggles the navigation menu open and closed */
Expand Down Expand Up @@ -135,7 +135,7 @@ class SidebarLinks extends React.Component {
const shouldFreeze = this.props.isSmallScreenWidth && !this.props.isDrawerOpen && this.isSidebarLoaded;
const optionListItems = SidebarUtils.getOrderedReportIDs(this.props.reportIDFromRoute);

const skeletonPlaceholder = <LHNSkeletonView shouldAnimate={!shouldFreeze} />;
const skeletonPlaceholder = <OptionsListSkeletonView shouldAnimate={!shouldFreeze} />;

return (
<View
Expand Down
Loading