Skip to content

Commit 83a448b

Browse files
authored
Merge pull request #26501 from rezkiy37/feature/24463-categories-ui-ux
Categories UI/UX
2 parents 666168c + b61d13a commit 83a448b

19 files changed

+280
-63
lines changed

src/CONST.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ const CONST = {
240240
TASKS: 'tasks',
241241
THREADS: 'threads',
242242
CUSTOM_STATUS: 'customStatus',
243+
NEW_DOT_CATEGORIES: 'newDotCategories',
243244
},
244245
BUTTON_STATES: {
245246
DEFAULT: 'default',

src/ONYXKEYS.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ type OnyxValues = {
375375
// Collections
376376
[ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download;
377377
[ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy;
378-
[ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: unknown;
378+
[ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory;
379379
[ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember;
380380
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories;
381381
[ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember;

src/components/CategoryPicker/categoryPickerPropTypes.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,23 @@ const propTypes = {
1414
/* Onyx Props */
1515
/** Collection of categories attached to a policy */
1616
policyCategories: PropTypes.objectOf(categoryPropTypes),
17+
18+
/* Onyx Props */
19+
/** Collection of recently used categories attached to a policy */
20+
policyRecentlyUsedCategories: PropTypes.arrayOf(PropTypes.string),
21+
22+
/* Onyx Props */
23+
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
24+
iou: PropTypes.shape({
25+
category: PropTypes.string.isRequired,
26+
}),
1727
};
1828

1929
const defaultProps = {
2030
policyID: '',
21-
policyCategories: null,
31+
policyCategories: {},
32+
policyRecentlyUsedCategories: [],
33+
iou: {},
2234
};
2335

2436
export {propTypes, defaultProps};
Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,88 @@
1-
import React, {useMemo} from 'react';
2-
import _ from 'underscore';
1+
import React, {useMemo, useState} from 'react';
32
import {withOnyx} from 'react-native-onyx';
3+
import _ from 'underscore';
4+
import lodashGet from 'lodash/get';
45
import ONYXKEYS from '../../ONYXKEYS';
56
import {propTypes, defaultProps} from './categoryPickerPropTypes';
6-
import OptionsList from '../OptionsList';
77
import styles from '../../styles/styles';
8-
import ScreenWrapper from '../ScreenWrapper';
98
import Navigation from '../../libs/Navigation/Navigation';
109
import ROUTES from '../../ROUTES';
10+
import CONST from '../../CONST';
11+
import * as IOU from '../../libs/actions/IOU';
12+
import * as OptionsListUtils from '../../libs/OptionsListUtils';
13+
import OptionsSelector from '../OptionsSelector';
14+
import useLocalize from '../../hooks/useLocalize';
15+
16+
function CategoryPicker({policyCategories, reportID, iouType, iou, policyRecentlyUsedCategories}) {
17+
const {translate} = useLocalize();
18+
const [searchValue, setSearchValue] = useState('');
19+
20+
const policyCategoriesCount = _.size(policyCategories);
21+
const isCategoriesCountBelowThreshold = policyCategoriesCount < CONST.CATEGORY_LIST_THRESHOLD;
1122

12-
function CategoryPicker({policyCategories, reportID, iouType}) {
13-
const sections = useMemo(() => {
14-
const categoryList = _.chain(policyCategories)
15-
.values()
16-
.map((category) => ({
17-
text: category.name,
18-
keyForList: category.name,
19-
tooltipText: category.name,
20-
}))
21-
.value();
23+
const selectedOptions = useMemo(() => {
24+
if (!iou.category) {
25+
return [];
26+
}
2227

2328
return [
2429
{
25-
data: categoryList,
30+
name: iou.category,
31+
enabled: true,
32+
accountID: null,
2633
},
2734
];
28-
}, [policyCategories]);
35+
}, [iou.category]);
36+
37+
const initialFocusedIndex = useMemo(() => {
38+
if (isCategoriesCountBelowThreshold && selectedOptions.length > 0) {
39+
return _.chain(policyCategories)
40+
.values()
41+
.findIndex((category) => category.name === selectedOptions[0].name, true)
42+
.value();
43+
}
44+
45+
return 0;
46+
}, [policyCategories, selectedOptions, isCategoriesCountBelowThreshold]);
47+
48+
const sections = useMemo(
49+
() => OptionsListUtils.getNewChatOptions({}, {}, [], searchValue, selectedOptions, [], false, false, true, policyCategories, policyRecentlyUsedCategories, false).categoryOptions,
50+
[policyCategories, policyRecentlyUsedCategories, searchValue, selectedOptions],
51+
);
52+
53+
const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, searchValue);
54+
const shouldShowTextInput = !isCategoriesCountBelowThreshold;
2955

3056
const navigateBack = () => {
3157
Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
3258
};
3359

60+
const updateCategory = (category) => {
61+
if (category.searchText === iou.category) {
62+
IOU.resetMoneyRequestCategory();
63+
} else {
64+
IOU.setMoneyRequestCategory(category.searchText);
65+
}
66+
67+
navigateBack();
68+
};
69+
3470
return (
35-
<ScreenWrapper includeSafeAreaPaddingBottom={false}>
36-
{({safeAreaPaddingBottomStyle}) => (
37-
<OptionsList
38-
optionHoveredStyle={styles.hoveredComponentBG}
39-
contentContainerStyles={[safeAreaPaddingBottomStyle]}
40-
sections={sections}
41-
onSelectRow={navigateBack}
42-
/>
43-
)}
44-
</ScreenWrapper>
71+
<OptionsSelector
72+
optionHoveredStyle={styles.hoveredComponentBG}
73+
sections={sections}
74+
selectedOptions={selectedOptions}
75+
value={searchValue}
76+
initialFocusedIndex={initialFocusedIndex}
77+
headerMessage={headerMessage}
78+
shouldShowTextInput={shouldShowTextInput}
79+
textInputLabel={translate('common.search')}
80+
boldStyle
81+
highlightSelectedOptions
82+
isRowMultilineSupported
83+
onChangeText={setSearchValue}
84+
onSelectRow={updateCategory}
85+
/>
4586
);
4687
}
4788

@@ -53,4 +94,10 @@ export default withOnyx({
5394
policyCategories: {
5495
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
5596
},
97+
policyRecentlyUsedCategories: {
98+
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`,
99+
},
100+
iou: {
101+
key: ONYXKEYS.IOU,
102+
},
56103
})(CategoryPicker);

src/components/MoneyRequestConfirmationList.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import ConfirmedRoute from './ConfirmedRoute';
3333
import transactionPropTypes from './transactionPropTypes';
3434
import DistanceRequestUtils from '../libs/DistanceRequestUtils';
3535
import * as IOU from '../libs/actions/IOU';
36+
import Permissions from '../libs/Permissions';
3637

3738
const propTypes = {
3839
/** Callback to inform parent modal of success */
@@ -90,6 +91,9 @@ const propTypes = {
9091
email: PropTypes.string.isRequired,
9192
}),
9293

94+
/** List of betas available to current user */
95+
betas: PropTypes.arrayOf(PropTypes.string),
96+
9397
/** The policyID of the request */
9498
policyID: PropTypes.string,
9599

@@ -144,6 +148,7 @@ const defaultProps = {
144148
session: {
145149
email: null,
146150
},
151+
betas: [],
147152
policyID: '',
148153
reportID: '',
149154
...withCurrentUserPersonalDetailsDefaultProps,
@@ -171,7 +176,7 @@ function MoneyRequestConfirmationList(props) {
171176
const {unit, rate, currency} = props.mileageRate;
172177
const distance = lodashGet(transaction, 'routes.route0.distance', 0);
173178
const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0;
174-
const shouldCategoryEditable = !_.isEmpty(props.policyCategories) && !props.isDistanceRequest;
179+
const shouldCategoryBeEditable = !_.isEmpty(props.policyCategories) && Permissions.canUseCategories(props.betas);
175180

176181
const formattedAmount = CurrencyUtils.convertToDisplayString(
177182
shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount,
@@ -484,7 +489,7 @@ function MoneyRequestConfirmationList(props) {
484489
disabled={didConfirm || props.isReadOnly || !isTypeRequest}
485490
/>
486491
)}
487-
{shouldCategoryEditable && (
492+
{shouldCategoryBeEditable && (
488493
<MenuItemWithTopDescription
489494
shouldShowRightIcon={!props.isReadOnly}
490495
title={props.iouCategory}
@@ -509,6 +514,9 @@ export default compose(
509514
session: {
510515
key: ONYXKEYS.SESSION,
511516
},
517+
betas: {
518+
key: ONYXKEYS.BETAS,
519+
},
512520
policyCategories: {
513521
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
514522
},

src/components/OptionRow.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ const propTypes = {
3939
/** Whether we should show the selected state */
4040
showSelectedState: PropTypes.bool,
4141

42+
/** Whether we highlight selected option */
43+
highlightSelected: PropTypes.bool,
44+
4245
/** Whether this item is selected */
4346
isSelected: PropTypes.bool,
4447

@@ -57,6 +60,9 @@ const propTypes = {
5760
/** Whether to remove the lateral padding and align the content with the margins */
5861
shouldDisableRowInnerPadding: PropTypes.bool,
5962

63+
/** Whether to wrap large text up to 2 lines */
64+
isMultilineSupported: PropTypes.bool,
65+
6066
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
6167

6268
...withLocalizePropTypes,
@@ -65,12 +71,14 @@ const propTypes = {
6571
const defaultProps = {
6672
hoverStyle: styles.sidebarLinkHover,
6773
showSelectedState: false,
74+
highlightSelected: false,
6875
isSelected: false,
6976
boldStyle: false,
7077
showTitleTooltip: false,
7178
onSelectRow: undefined,
7279
isDisabled: false,
7380
optionIsFocused: false,
81+
isMultilineSupported: false,
7482
style: null,
7583
shouldHaveOptionSeparator: false,
7684
shouldDisableRowInnerPadding: false,
@@ -89,9 +97,11 @@ class OptionRow extends Component {
8997
return (
9098
this.state.isDisabled !== nextState.isDisabled ||
9199
this.props.isDisabled !== nextProps.isDisabled ||
100+
this.props.isMultilineSupported !== nextProps.isMultilineSupported ||
92101
this.props.isSelected !== nextProps.isSelected ||
93102
this.props.shouldHaveOptionSeparator !== nextProps.shouldHaveOptionSeparator ||
94103
this.props.showSelectedState !== nextProps.showSelectedState ||
104+
this.props.highlightSelected !== nextProps.highlightSelected ||
95105
this.props.showTitleTooltip !== nextProps.showTitleTooltip ||
96106
!_.isEqual(this.props.option.icons, nextProps.option.icons) ||
97107
this.props.optionIsFocused !== nextProps.optionIsFocused ||
@@ -119,7 +129,7 @@ class OptionRow extends Component {
119129
let pressableRef = null;
120130
const textStyle = this.props.optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
121131
const textUnreadStyle = this.props.boldStyle || this.props.option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle];
122-
const displayNameStyle = StyleUtils.combineStyles(styles.optionDisplayName, textUnreadStyle, this.props.style, styles.pre);
132+
const displayNameStyle = StyleUtils.combineStyles(styles.optionDisplayName, textUnreadStyle, this.props.style, styles.pre, this.state.isDisabled ? styles.optionRowDisabled : {});
123133
const alternateTextStyle = StyleUtils.combineStyles(
124134
textStyle,
125135
styles.optionAlternateText,
@@ -182,6 +192,7 @@ class OptionRow extends Component {
182192
this.props.optionIsFocused ? styles.sidebarLinkActive : null,
183193
this.props.shouldHaveOptionSeparator && styles.borderTop,
184194
!this.props.onSelectRow && !this.props.isDisabled ? styles.cursorDefault : null,
195+
this.props.isSelected && this.props.highlightSelected && styles.optionRowSelected,
185196
]}
186197
accessibilityLabel={this.props.option.text}
187198
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
@@ -216,7 +227,7 @@ class OptionRow extends Component {
216227
fullTitle={this.props.option.text}
217228
displayNamesWithTooltips={displayNamesWithTooltips}
218229
tooltipEnabled={this.props.showTitleTooltip}
219-
numberOfLines={1}
230+
numberOfLines={this.props.isMultilineSupported ? 2 : 1}
220231
textStyles={displayNameStyle}
221232
shouldUseFullTitle={
222233
this.props.option.isChatRoom ||
@@ -249,6 +260,14 @@ class OptionRow extends Component {
249260
</View>
250261
)}
251262
{this.props.showSelectedState && <SelectCircle isChecked={this.props.isSelected} />}
263+
{this.props.isSelected && this.props.highlightSelected && (
264+
<View style={styles.defaultCheckmarkWrapper}>
265+
<Icon
266+
src={Expensicons.Checkmark}
267+
fill={themeColors.iconSuccessFill}
268+
/>
269+
</View>
270+
)}
252271
</View>
253272
</View>
254273
{Boolean(this.props.option.customIcon) && (

src/components/OptionsList/BaseOptionsList.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,12 @@ function BaseOptionsList({
5656
shouldDisableRowInnerPadding,
5757
disableFocusOptions,
5858
canSelectMultipleOptions,
59+
highlightSelectedOptions,
5960
onSelectRow,
6061
boldStyle,
6162
isDisabled,
6263
innerRef,
64+
isRowMultilineSupported,
6365
}) {
6466
const flattenedData = useRef();
6567
const previousSections = usePrevious(sections);
@@ -175,12 +177,14 @@ function BaseOptionsList({
175177
hoverStyle={optionHoveredStyle}
176178
optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset}
177179
onSelectRow={onSelectRow}
178-
isSelected={Boolean(_.find(selectedOptions, (option) => option.accountID === item.accountID))}
180+
isSelected={Boolean(_.find(selectedOptions, (option) => option.accountID === item.accountID || option.name === item.searchText))}
179181
showSelectedState={canSelectMultipleOptions}
182+
highlightSelected={highlightSelectedOptions}
180183
boldStyle={boldStyle}
181184
isDisabled={isItemDisabled}
182185
shouldHaveOptionSeparator={index > 0 && shouldHaveOptionSeparator}
183186
shouldDisableRowInnerPadding={shouldDisableRowInnerPadding}
187+
isMultilineSupported={isRowMultilineSupported}
184188
/>
185189
);
186190
};

src/components/OptionsList/optionsListPropTypes.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const propTypes = {
4040
/** Whether we can select multiple options or not */
4141
canSelectMultipleOptions: PropTypes.bool,
4242

43+
/** Whether we highlight selected options */
44+
highlightSelectedOptions: PropTypes.bool,
45+
4346
/** Whether to show headers above each section or not */
4447
hideSectionHeaders: PropTypes.bool,
4548

@@ -78,6 +81,9 @@ const propTypes = {
7881

7982
/** Whether to show the scroll bar */
8083
showScrollIndicator: PropTypes.bool,
84+
85+
/** Whether to wrap large text up to 2 lines */
86+
isRowMultilineSupported: PropTypes.bool,
8187
};
8288

8389
const defaultProps = {
@@ -88,6 +94,7 @@ const defaultProps = {
8894
focusedIndex: 0,
8995
selectedOptions: [],
9096
canSelectMultipleOptions: false,
97+
highlightSelectedOptions: false,
9198
hideSectionHeaders: false,
9299
disableFocusOptions: false,
93100
boldStyle: false,
@@ -101,6 +108,7 @@ const defaultProps = {
101108
shouldHaveOptionSeparator: false,
102109
shouldDisableRowInnerPadding: false,
103110
showScrollIndicator: false,
111+
isRowMultilineSupported: false,
104112
};
105113

106114
export {propTypes, defaultProps};

0 commit comments

Comments
 (0)