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

Limit tag list to 500 items #31447

Merged
merged 32 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c570773
add a limit const
rezkiy37 Nov 14, 2023
8e0ab70
implement a limit
rezkiy37 Nov 14, 2023
f12d261
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Nov 15, 2023
a5c9de5
Revert "implement a limit"
rezkiy37 Nov 15, 2023
5d07521
Create ShowMore component
rezkiy37 Nov 16, 2023
c8928e7
add strings
rezkiy37 Nov 16, 2023
653deac
add ability to pass footer to OptionsList
rezkiy37 Nov 16, 2023
b95cf0e
integrate pagination for OptionsSelector
rezkiy37 Nov 16, 2023
a820d4e
rename a const
rezkiy37 Nov 16, 2023
ffd144e
integrate inserts for TagPicker and enable pagination
rezkiy37 Nov 16, 2023
82c9f1c
reuse ShowMore
rezkiy37 Nov 16, 2023
a1a62c7
allow pass container style for ShowMore
rezkiy37 Nov 16, 2023
0bcc5a9
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Nov 17, 2023
09e72c7
add periods
rezkiy37 Nov 17, 2023
e2d780d
remove empty lines
rezkiy37 Nov 17, 2023
19ca4ac
calculate all visible options
rezkiy37 Nov 17, 2023
2cde176
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Nov 17, 2023
c29740a
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Nov 20, 2023
19ff8d8
describe prop better
rezkiy37 Nov 20, 2023
059a302
convert getter to methods
rezkiy37 Nov 20, 2023
8f952bc
increase bottom spacing
rezkiy37 Nov 20, 2023
1de984d
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Nov 21, 2023
0f794ec
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Nov 22, 2023
b08423a
integrate limitation generally
rezkiy37 Nov 22, 2023
9520db9
fix component did update
rezkiy37 Nov 22, 2023
3234a9b
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Nov 22, 2023
96c23a4
convert to built in feature
rezkiy37 Nov 22, 2023
f99086a
rename const
rezkiy37 Nov 25, 2023
49154ff
minor updates BaseOptionsSelector
rezkiy37 Nov 25, 2023
b4d1537
rename ShowMoreButton
rezkiy37 Nov 25, 2023
1080ca0
clarify comments
rezkiy37 Nov 25, 2023
c4f1f63
clarify comment
rezkiy37 Nov 27, 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
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2902,6 +2902,12 @@ const CONST = {

BACK_BUTTON_NATIVE_ID: 'backButton',

/**
* The maximum count of items per page for OptionsSelector.
* When paginate, it multiplies by page number.
*/
MAX_OPTIONS_SELECTOR_PAGE_LENGTH: 500,

/**
* Performance test setup - run the same test multiple times to get a more accurate result
*/
Expand Down
21 changes: 5 additions & 16 deletions src/components/MoneyRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,16 @@ import * as IOU from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import Button from './Button';
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
import categoryPropTypes from './categoryPropTypes';
import ConfirmedRoute from './ConfirmedRoute';
import FormHelpMessage from './FormHelpMessage';
import * as Expensicons from './Icon/Expensicons';
import Image from './Image';
import MenuItemWithTopDescription from './MenuItemWithTopDescription';
import optionPropTypes from './optionPropTypes';
import OptionsSelector from './OptionsSelector';
import SettlementButton from './SettlementButton';
import ShowMoreButton from './ShowMoreButton';
import Switch from './Switch';
import tagPropTypes from './tagPropTypes';
import Text from './Text';
Expand Down Expand Up @@ -636,20 +635,10 @@ function MoneyRequestConfirmationList(props) {
numberOfLinesTitle={2}
/>
{!shouldShowAllFields && (
<View style={[styles.flexRow, styles.justifyContentBetween, styles.mh3, styles.alignItemsCenter, styles.mb2, styles.mt1]}>
<View style={[styles.shortTermsHorizontalRule, styles.flex1, styles.mr0]} />
<Button
small
onPress={toggleShouldExpandFields}
text={translate('common.showMore')}
shouldShowRightIcon
iconRight={Expensicons.DownArrow}
iconFill={theme.icon}
style={styles.mh0}
/>

<View style={[styles.shortTermsHorizontalRule, styles.flex1, styles.ml0]} />
</View>
<ShowMoreButton
containerStyle={styles.mt1}
Copy link
Contributor

Choose a reason for hiding this comment

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

Bottom margin missed here which caused #33758

onPress={toggleShouldExpandFields}
/>
)}
{shouldShowAllFields && (
<>
Expand Down
2 changes: 2 additions & 0 deletions src/components/OptionsList/BaseOptionsList.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function BaseOptionsList({
isLoadingNewOptions,
nestedScrollEnabled,
bounces,
renderFooterContent,
}) {
const styles = useThemeStyles();
const flattenedData = useRef();
Expand Down Expand Up @@ -286,6 +287,7 @@ function BaseOptionsList({
viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}}
onViewableItemsChanged={onViewableItemsChanged}
bounces={bounces}
ListFooterComponent={renderFooterContent}
/>
</>
)}
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 @@ -100,6 +100,9 @@ const propTypes = {

/** Whether the list should have a bounce effect on iOS */
bounces: PropTypes.bool,

/** Custom content to display in the floating footer */
renderFooterContent: PropTypes.func,
};

const defaultProps = {
Expand Down Expand Up @@ -130,6 +133,7 @@ const defaultProps = {
isLoadingNewOptions: false,
nestedScrollEnabled: true,
bounces: true,
renderFooterContent: undefined,
};

export {propTypes, defaultProps};
85 changes: 81 additions & 4 deletions src/components/OptionsSelector/BaseOptionsSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Icon from '@components/Icon';
import {Info} from '@components/Icon/Expensicons';
import OptionsList from '@components/OptionsList';
import {PressableWithoutFeedback} from '@components/Pressable';
import ShowMoreButton from '@components/ShowMoreButton';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
Expand Down Expand Up @@ -74,17 +75,23 @@ class BaseOptionsSelector extends Component {
this.selectFocusedOption = this.selectFocusedOption.bind(this);
this.addToSelection = this.addToSelection.bind(this);
this.updateSearchValue = this.updateSearchValue.bind(this);
this.incrementPage = this.incrementPage.bind(this);
this.sliceSections = this.sliceSections.bind(this);
this.calculateAllVisibleOptionsCount = this.calculateAllVisibleOptionsCount.bind(this);
this.relatedTarget = null;

const allOptions = this.flattenSections();
const sections = this.sliceSections();
const focusedIndex = this.getInitiallyFocusedIndex(allOptions);

this.state = {
sections,
allOptions,
focusedIndex,
shouldDisableRowSelection: false,
shouldShowReferralModal: false,
errorMessage: '',
paginationPage: 1,
};
}

Expand All @@ -100,7 +107,7 @@ class BaseOptionsSelector extends Component {
this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false);
}

componentDidUpdate(prevProps) {
componentDidUpdate(prevProps, prevState) {
if (prevProps.isFocused !== this.props.isFocused) {
if (this.props.isFocused) {
this.subscribeToKeyboardShortcut();
Expand All @@ -118,14 +125,24 @@ class BaseOptionsSelector extends Component {
}, CONST.ANIMATED_TRANSITION);
}

if (prevState.paginationPage !== this.state.paginationPage) {
const newSections = this.sliceSections();

this.setState({
sections: newSections,
});
}

if (_.isEqual(this.props.sections, prevProps.sections)) {
return;
}

const newSections = this.sliceSections();
const newOptions = this.flattenSections();

if (prevProps.preferredLocale !== this.props.preferredLocale) {
this.setState({
sections: newSections,
allOptions: newOptions,
});
return;
Expand All @@ -136,6 +153,7 @@ class BaseOptionsSelector extends Component {
// eslint-disable-next-line react/no-did-update-set-state
this.setState(
{
sections: newSections,
allOptions: newOptions,
focusedIndex: _.isNumber(this.props.initialFocusedIndex) ? this.props.initialFocusedIndex : newFocusedIndex,
},
Expand Down Expand Up @@ -189,8 +207,43 @@ class BaseOptionsSelector extends Component {
return defaultIndex;
}

/**
* Maps sections to render only allowed count of them per section.
*
* @returns {Objects[]}
*/
sliceSections() {
return _.map(this.props.sections, (section) => {
if (_.isEmpty(section.data)) {
return section;
}

return {
...section,
data: section.data.slice(0, CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * lodashGet(this.state, 'paginationPage', 1)),
};
});
}

/**
* Calculates all currently visible options based on the sections that are currently being shown
* and the number of items of those sections.
*
* @returns {Number}
*/
calculateAllVisibleOptionsCount() {
let count = 0;

_.forEach(this.state.sections, (section) => {
count += lodashGet(section, 'data.length', 0);
});

return count;
}

updateSearchValue(value) {
this.setState({
paginationPage: 1,
errorMessage: value.length > this.props.maxLength ? this.props.translate('common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}) : '',
});

Expand Down Expand Up @@ -328,12 +381,16 @@ class BaseOptionsSelector extends Component {
const itemIndex = option.index;
const sectionIndex = option.sectionIndex;

if (!lodashGet(this.state.sections, `[${sectionIndex}].data[${itemIndex}]`, null)) {
return;
}

// Note: react-native's SectionList automatically strips out any empty sections.
// So we need to reduce the sectionIndex to remove any empty sections in front of the one we're trying to scroll to.
// Otherwise, it will cause an index-out-of-bounds error and crash the app.
let adjustedSectionIndex = sectionIndex;
for (let i = 0; i < sectionIndex; i++) {
if (_.isEmpty(lodashGet(this.props.sections, `[${i}].data`))) {
if (_.isEmpty(lodashGet(this.state.sections, `[${i}].data`))) {
adjustedSectionIndex--;
}
}
Expand Down Expand Up @@ -387,7 +444,17 @@ class BaseOptionsSelector extends Component {
this.props.onAddToSelection(option);
}

/**
* Increments a pagination page to show more items
*/
incrementPage() {
this.setState((prev) => ({
paginationPage: prev.paginationPage + 1,
}));
}

render() {
const shouldShowShowMoreButton = this.state.allOptions.length > CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * this.state.paginationPage;
const shouldShowFooter =
!this.props.isReadOnly && (this.props.shouldShowConfirmButton || this.props.footerContent) && !(this.props.canSelectMultipleOptions && _.isEmpty(this.props.selectedOptions));
const defaultConfirmButtonText = _.isUndefined(this.props.confirmButtonText) ? this.props.translate('common.confirm') : this.props.confirmButtonText;
Expand Down Expand Up @@ -424,7 +491,7 @@ class BaseOptionsSelector extends Component {
ref={(el) => (this.list = el)}
optionHoveredStyle={this.props.optionHoveredStyle}
onSelectRow={this.props.onSelectRow ? this.selectRow : undefined}
sections={this.props.sections}
sections={this.state.sections}
focusedIndex={this.state.focusedIndex}
selectedOptions={this.props.selectedOptions}
canSelectMultipleOptions={this.props.canSelectMultipleOptions}
Expand Down Expand Up @@ -458,6 +525,16 @@ class BaseOptionsSelector extends Component {
shouldPreventDefaultFocusOnSelectRow={this.props.shouldPreventDefaultFocusOnSelectRow}
nestedScrollEnabled={this.props.nestedScrollEnabled}
bounces={!this.props.shouldTextInputAppearBelowOptions || !this.props.shouldAllowScrollingChildren}
renderFooterContent={() =>
shouldShowShowMoreButton && (
<ShowMoreButton
containerStyle={{...styles.mt2, ...styles.mb5}}
currentCount={CONST.MAX_OPTIONS_SELECTOR_PAGE_LENGTH * this.state.paginationPage}
totalCount={this.state.allOptions.length}
onPress={this.incrementPage}
/>
)
}
/>
);

Expand All @@ -475,7 +552,7 @@ class BaseOptionsSelector extends Component {
<ArrowKeyFocusManager
disabledIndexes={this.disabledOptionsIndexes}
focusedIndex={this.state.focusedIndex}
maxIndex={this.state.allOptions.length - 1}
maxIndex={this.calculateAllVisibleOptionsCount() - 1}
onFocusedIndexChanged={this.props.disableArrowKeysActions ? () => {} : this.updateFocusedIndex}
shouldResetIndexOnEndReached={false}
>
Expand Down
70 changes: 70 additions & 0 deletions src/components/ShowMoreButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import PropTypes from 'prop-types';
import React from 'react';
import {Text, View} from 'react-native';
import _ from 'underscore';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
import useLocalize from '@hooks/useLocalize';
import * as NumberFormatUtils from '@libs/NumberFormatUtils';
import stylePropTypes from '@styles/stylePropTypes';
import styles from '@styles/styles';
import themeColors from '@styles/themes/default';

const propTypes = {
/** Additional styles for container */
containerStyle: stylePropTypes,

/** The number of currently shown items */
currentCount: PropTypes.number,

/** The total number of items that could be shown */
totalCount: PropTypes.number,

/** A handler that fires when button has been pressed */
onPress: PropTypes.func.isRequired,
};

const defaultProps = {
containerStyle: {},
currentCount: undefined,
totalCount: undefined,
};

function ShowMoreButton({containerStyle, currentCount, totalCount, onPress}) {
const {translate, preferredLocale} = useLocalize();

const shouldShowCounter = _.isNumber(currentCount) && _.isNumber(totalCount);

return (
<View style={[styles.alignItemsCenter, containerStyle]}>
{shouldShowCounter && (
<Text style={[styles.mb2, styles.textLabelSupporting]}>
{`${translate('common.showing')} `}
<Text style={styles.textStrong}>{currentCount}</Text>
{` ${translate('common.of')} `}
<Text style={styles.textStrong}>{NumberFormatUtils.format(preferredLocale, totalCount)}</Text>
</Text>
)}
<View style={[styles.w100, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}>
<View style={[styles.shortTermsHorizontalRule, styles.flex1, styles.mr0]} />
<Button
style={styles.mh0}
small
shouldShowRightIcon
iconFill={themeColors.icon}
iconRight={Expensicons.DownArrow}
text={translate('common.showMore')}
accessibilityLabel={translate('common.showMore')}
onPress={onPress}
/>
<View style={[styles.shortTermsHorizontalRule, styles.flex1, styles.ml0]} />
</View>
</View>
);
}

ShowMoreButton.displayName = 'ShowMoreButton';
ShowMoreButton.propTypes = propTypes;
ShowMoreButton.defaultProps = defaultProps;

export default ShowMoreButton;
4 changes: 3 additions & 1 deletion src/components/TagPicker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import OptionsSelector from '@components/OptionsSelector';
import useLocalize from '@hooks/useLocalize';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as StyleUtils from '@styles/StyleUtils';
import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {defaultProps, propTypes} from './tagPickerPropTypes';

function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, onSubmit, shouldShowDisabledAndSelectedOption}) {
function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption, insets, onSubmit}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');
Expand Down Expand Up @@ -66,6 +67,7 @@ function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, onSubm

return (
<OptionsSelector
contentContainerStyles={[{paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}]}
optionHoveredStyle={styles.hoveredComponentBG}
sectionHeaderStyle={styles.mt5}
sections={sections}
Expand Down
7 changes: 7 additions & 0 deletions src/components/TagPicker/tagPickerPropTypes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import tagPropTypes from '@components/tagPropTypes';
import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes';

const propTypes = {
/** The policyID we are getting tags for */
Expand All @@ -14,6 +15,12 @@ const propTypes = {
/** Callback to submit the selected tag */
onSubmit: PropTypes.func.isRequired,

/**
* Safe area insets required for reflecting the portion of the view,
* that is not covered by navigation bars, tab bars, toolbars, and other ancestor views.
*/
insets: safeAreaInsetPropTypes.isRequired,

/* Onyx Props */
/** Collection of tags attached to a policy */
policyTags: tagPropTypes,
Expand Down
Loading
Loading