diff --git a/src/CONST.ts b/src/CONST.ts
index 5bb2b06360d7..62b0dc4af07c 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -240,6 +240,7 @@ const CONST = {
TASKS: 'tasks',
THREADS: 'threads',
CUSTOM_STATUS: 'customStatus',
+ NEW_DOT_CATEGORIES: 'newDotCategories',
},
BUTTON_STATES: {
DEFAULT: 'default',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index a2809c439931..ddc2aa4de620 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -375,7 +375,7 @@ type OnyxValues = {
// Collections
[ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download;
[ONYXKEYS.COLLECTION.POLICY]: OnyxTypes.Policy;
- [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: unknown;
+ [ONYXKEYS.COLLECTION.POLICY_CATEGORIES]: OnyxTypes.PolicyCategory;
[ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMember;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories;
[ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMember;
diff --git a/src/components/CategoryPicker/categoryPickerPropTypes.js b/src/components/CategoryPicker/categoryPickerPropTypes.js
index ccc1643021ce..b8e24c199a73 100644
--- a/src/components/CategoryPicker/categoryPickerPropTypes.js
+++ b/src/components/CategoryPicker/categoryPickerPropTypes.js
@@ -14,11 +14,23 @@ const propTypes = {
/* Onyx Props */
/** Collection of categories attached to a policy */
policyCategories: PropTypes.objectOf(categoryPropTypes),
+
+ /* Onyx Props */
+ /** Collection of recently used categories attached to a policy */
+ policyRecentlyUsedCategories: PropTypes.arrayOf(PropTypes.string),
+
+ /* Onyx Props */
+ /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
+ iou: PropTypes.shape({
+ category: PropTypes.string.isRequired,
+ }),
};
const defaultProps = {
policyID: '',
- policyCategories: null,
+ policyCategories: {},
+ policyRecentlyUsedCategories: [],
+ iou: {},
};
export {propTypes, defaultProps};
diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js
index 163ab6673ca2..91c7e82e7887 100644
--- a/src/components/CategoryPicker/index.js
+++ b/src/components/CategoryPicker/index.js
@@ -1,47 +1,88 @@
-import React, {useMemo} from 'react';
-import _ from 'underscore';
+import React, {useMemo, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
+import lodashGet from 'lodash/get';
import ONYXKEYS from '../../ONYXKEYS';
import {propTypes, defaultProps} from './categoryPickerPropTypes';
-import OptionsList from '../OptionsList';
import styles from '../../styles/styles';
-import ScreenWrapper from '../ScreenWrapper';
import Navigation from '../../libs/Navigation/Navigation';
import ROUTES from '../../ROUTES';
+import CONST from '../../CONST';
+import * as IOU from '../../libs/actions/IOU';
+import * as OptionsListUtils from '../../libs/OptionsListUtils';
+import OptionsSelector from '../OptionsSelector';
+import useLocalize from '../../hooks/useLocalize';
+
+function CategoryPicker({policyCategories, reportID, iouType, iou, policyRecentlyUsedCategories}) {
+ const {translate} = useLocalize();
+ const [searchValue, setSearchValue] = useState('');
+
+ const policyCategoriesCount = _.size(policyCategories);
+ const isCategoriesCountBelowThreshold = policyCategoriesCount < CONST.CATEGORY_LIST_THRESHOLD;
-function CategoryPicker({policyCategories, reportID, iouType}) {
- const sections = useMemo(() => {
- const categoryList = _.chain(policyCategories)
- .values()
- .map((category) => ({
- text: category.name,
- keyForList: category.name,
- tooltipText: category.name,
- }))
- .value();
+ const selectedOptions = useMemo(() => {
+ if (!iou.category) {
+ return [];
+ }
return [
{
- data: categoryList,
+ name: iou.category,
+ enabled: true,
+ accountID: null,
},
];
- }, [policyCategories]);
+ }, [iou.category]);
+
+ const initialFocusedIndex = useMemo(() => {
+ if (isCategoriesCountBelowThreshold && selectedOptions.length > 0) {
+ return _.chain(policyCategories)
+ .values()
+ .findIndex((category) => category.name === selectedOptions[0].name, true)
+ .value();
+ }
+
+ return 0;
+ }, [policyCategories, selectedOptions, isCategoriesCountBelowThreshold]);
+
+ const sections = useMemo(
+ () => OptionsListUtils.getNewChatOptions({}, {}, [], searchValue, selectedOptions, [], false, false, true, policyCategories, policyRecentlyUsedCategories, false).categoryOptions,
+ [policyCategories, policyRecentlyUsedCategories, searchValue, selectedOptions],
+ );
+
+ const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, searchValue);
+ const shouldShowTextInput = !isCategoriesCountBelowThreshold;
const navigateBack = () => {
Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
};
+ const updateCategory = (category) => {
+ if (category.searchText === iou.category) {
+ IOU.resetMoneyRequestCategory();
+ } else {
+ IOU.setMoneyRequestCategory(category.searchText);
+ }
+
+ navigateBack();
+ };
+
return (
-
- {({safeAreaPaddingBottomStyle}) => (
-
- )}
-
+
);
}
@@ -53,4 +94,10 @@ export default withOnyx({
policyCategories: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
},
+ policyRecentlyUsedCategories: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`,
+ },
+ iou: {
+ key: ONYXKEYS.IOU,
+ },
})(CategoryPicker);
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index a7695c939907..87dc0e6795b4 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -33,6 +33,7 @@ import ConfirmedRoute from './ConfirmedRoute';
import transactionPropTypes from './transactionPropTypes';
import DistanceRequestUtils from '../libs/DistanceRequestUtils';
import * as IOU from '../libs/actions/IOU';
+import Permissions from '../libs/Permissions';
const propTypes = {
/** Callback to inform parent modal of success */
@@ -90,6 +91,9 @@ const propTypes = {
email: PropTypes.string.isRequired,
}),
+ /** List of betas available to current user */
+ betas: PropTypes.arrayOf(PropTypes.string),
+
/** The policyID of the request */
policyID: PropTypes.string,
@@ -144,6 +148,7 @@ const defaultProps = {
session: {
email: null,
},
+ betas: [],
policyID: '',
reportID: '',
...withCurrentUserPersonalDetailsDefaultProps,
@@ -171,7 +176,7 @@ function MoneyRequestConfirmationList(props) {
const {unit, rate, currency} = props.mileageRate;
const distance = lodashGet(transaction, 'routes.route0.distance', 0);
const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0;
- const shouldCategoryEditable = !_.isEmpty(props.policyCategories) && !props.isDistanceRequest;
+ const shouldCategoryBeEditable = !_.isEmpty(props.policyCategories) && Permissions.canUseCategories(props.betas);
const formattedAmount = CurrencyUtils.convertToDisplayString(
shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount,
@@ -484,7 +489,7 @@ function MoneyRequestConfirmationList(props) {
disabled={didConfirm || props.isReadOnly || !isTypeRequest}
/>
)}
- {shouldCategoryEditable && (
+ {shouldCategoryBeEditable && (
`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
},
diff --git a/src/components/OptionRow.js b/src/components/OptionRow.js
index adaa4457bbd9..50aff23dc9d0 100644
--- a/src/components/OptionRow.js
+++ b/src/components/OptionRow.js
@@ -39,6 +39,9 @@ const propTypes = {
/** Whether we should show the selected state */
showSelectedState: PropTypes.bool,
+ /** Whether we highlight selected option */
+ highlightSelected: PropTypes.bool,
+
/** Whether this item is selected */
isSelected: PropTypes.bool,
@@ -57,6 +60,9 @@ const propTypes = {
/** Whether to remove the lateral padding and align the content with the margins */
shouldDisableRowInnerPadding: PropTypes.bool,
+ /** Whether to wrap large text up to 2 lines */
+ isMultilineSupported: PropTypes.bool,
+
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
...withLocalizePropTypes,
@@ -65,12 +71,14 @@ const propTypes = {
const defaultProps = {
hoverStyle: styles.sidebarLinkHover,
showSelectedState: false,
+ highlightSelected: false,
isSelected: false,
boldStyle: false,
showTitleTooltip: false,
onSelectRow: undefined,
isDisabled: false,
optionIsFocused: false,
+ isMultilineSupported: false,
style: null,
shouldHaveOptionSeparator: false,
shouldDisableRowInnerPadding: false,
@@ -89,9 +97,11 @@ class OptionRow extends Component {
return (
this.state.isDisabled !== nextState.isDisabled ||
this.props.isDisabled !== nextProps.isDisabled ||
+ this.props.isMultilineSupported !== nextProps.isMultilineSupported ||
this.props.isSelected !== nextProps.isSelected ||
this.props.shouldHaveOptionSeparator !== nextProps.shouldHaveOptionSeparator ||
this.props.showSelectedState !== nextProps.showSelectedState ||
+ this.props.highlightSelected !== nextProps.highlightSelected ||
this.props.showTitleTooltip !== nextProps.showTitleTooltip ||
!_.isEqual(this.props.option.icons, nextProps.option.icons) ||
this.props.optionIsFocused !== nextProps.optionIsFocused ||
@@ -119,7 +129,7 @@ class OptionRow extends Component {
let pressableRef = null;
const textStyle = this.props.optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
const textUnreadStyle = this.props.boldStyle || this.props.option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle];
- const displayNameStyle = StyleUtils.combineStyles(styles.optionDisplayName, textUnreadStyle, this.props.style, styles.pre);
+ const displayNameStyle = StyleUtils.combineStyles(styles.optionDisplayName, textUnreadStyle, this.props.style, styles.pre, this.state.isDisabled ? styles.optionRowDisabled : {});
const alternateTextStyle = StyleUtils.combineStyles(
textStyle,
styles.optionAlternateText,
@@ -182,6 +192,7 @@ class OptionRow extends Component {
this.props.optionIsFocused ? styles.sidebarLinkActive : null,
this.props.shouldHaveOptionSeparator && styles.borderTop,
!this.props.onSelectRow && !this.props.isDisabled ? styles.cursorDefault : null,
+ this.props.isSelected && this.props.highlightSelected && styles.optionRowSelected,
]}
accessibilityLabel={this.props.option.text}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
@@ -216,7 +227,7 @@ class OptionRow extends Component {
fullTitle={this.props.option.text}
displayNamesWithTooltips={displayNamesWithTooltips}
tooltipEnabled={this.props.showTitleTooltip}
- numberOfLines={1}
+ numberOfLines={this.props.isMultilineSupported ? 2 : 1}
textStyles={displayNameStyle}
shouldUseFullTitle={
this.props.option.isChatRoom ||
@@ -249,6 +260,14 @@ class OptionRow extends Component {
)}
{this.props.showSelectedState && }
+ {this.props.isSelected && this.props.highlightSelected && (
+
+
+
+ )}
{Boolean(this.props.option.customIcon) && (
diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js
index 8f63cfc29959..252b015edd45 100644
--- a/src/components/OptionsList/BaseOptionsList.js
+++ b/src/components/OptionsList/BaseOptionsList.js
@@ -56,10 +56,12 @@ function BaseOptionsList({
shouldDisableRowInnerPadding,
disableFocusOptions,
canSelectMultipleOptions,
+ highlightSelectedOptions,
onSelectRow,
boldStyle,
isDisabled,
innerRef,
+ isRowMultilineSupported,
}) {
const flattenedData = useRef();
const previousSections = usePrevious(sections);
@@ -175,12 +177,14 @@ function BaseOptionsList({
hoverStyle={optionHoveredStyle}
optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset}
onSelectRow={onSelectRow}
- isSelected={Boolean(_.find(selectedOptions, (option) => option.accountID === item.accountID))}
+ isSelected={Boolean(_.find(selectedOptions, (option) => option.accountID === item.accountID || option.name === item.searchText))}
showSelectedState={canSelectMultipleOptions}
+ highlightSelected={highlightSelectedOptions}
boldStyle={boldStyle}
isDisabled={isItemDisabled}
shouldHaveOptionSeparator={index > 0 && shouldHaveOptionSeparator}
shouldDisableRowInnerPadding={shouldDisableRowInnerPadding}
+ isMultilineSupported={isRowMultilineSupported}
/>
);
};
diff --git a/src/components/OptionsList/optionsListPropTypes.js b/src/components/OptionsList/optionsListPropTypes.js
index 74c13c7f2455..a2479c878041 100644
--- a/src/components/OptionsList/optionsListPropTypes.js
+++ b/src/components/OptionsList/optionsListPropTypes.js
@@ -40,6 +40,9 @@ const propTypes = {
/** Whether we can select multiple options or not */
canSelectMultipleOptions: PropTypes.bool,
+ /** Whether we highlight selected options */
+ highlightSelectedOptions: PropTypes.bool,
+
/** Whether to show headers above each section or not */
hideSectionHeaders: PropTypes.bool,
@@ -78,6 +81,9 @@ const propTypes = {
/** Whether to show the scroll bar */
showScrollIndicator: PropTypes.bool,
+
+ /** Whether to wrap large text up to 2 lines */
+ isRowMultilineSupported: PropTypes.bool,
};
const defaultProps = {
@@ -88,6 +94,7 @@ const defaultProps = {
focusedIndex: 0,
selectedOptions: [],
canSelectMultipleOptions: false,
+ highlightSelectedOptions: false,
hideSectionHeaders: false,
disableFocusOptions: false,
boldStyle: false,
@@ -101,6 +108,7 @@ const defaultProps = {
shouldHaveOptionSeparator: false,
shouldDisableRowInnerPadding: false,
showScrollIndicator: false,
+ isRowMultilineSupported: false,
};
export {propTypes, defaultProps};
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index 338de90a0509..ee3840bff69d 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -139,6 +139,10 @@ class BaseOptionsSelector extends Component {
* @returns {Number}
*/
getInitiallyFocusedIndex(allOptions) {
+ if (_.isNumber(this.props.initialFocusedIndex)) {
+ return this.props.initialFocusedIndex;
+ }
+
if (this.props.selectedOptions.length > 0) {
return this.props.selectedOptions.length;
}
@@ -374,6 +378,7 @@ class BaseOptionsSelector extends Component {
showTitleTooltip={this.props.showTitleTooltip}
isDisabled={this.props.isDisabled}
shouldHaveOptionSeparator={this.props.shouldHaveOptionSeparator}
+ highlightSelectedOptions={this.props.highlightSelectedOptions}
onLayout={() => {
if (this.props.selectedOptions.length === 0) {
this.scrollToIndex(this.state.focusedIndex, false);
@@ -388,6 +393,7 @@ class BaseOptionsSelector extends Component {
listStyles={this.props.listStyles}
isLoading={!this.props.shouldShowOptions}
showScrollIndicator={this.props.showScrollIndicator}
+ isRowMultilineSupported={this.props.isRowMultilineSupported}
/>
);
return (
diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js
index 02b807bf66c1..dff0a16c0b3e 100644
--- a/src/components/OptionsSelector/optionsSelectorPropTypes.js
+++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js
@@ -53,6 +53,9 @@ const propTypes = {
/** Whether we can select multiple options */
canSelectMultipleOptions: PropTypes.bool,
+ /** Whether we highlight selected options */
+ highlightSelectedOptions: PropTypes.bool,
+
/** Whether any section headers should be visible */
hideSectionHeaders: PropTypes.bool,
@@ -106,6 +109,12 @@ const propTypes = {
/** Whether to use default padding and flex styles for children */
shouldUseStyleForChildren: PropTypes.bool,
+
+ /** Whether to wrap large text up to 2 lines */
+ isRowMultilineSupported: PropTypes.bool,
+
+ /** Initial focused index value */
+ initialFocusedIndex: PropTypes.number,
};
const defaultProps = {
@@ -116,6 +125,7 @@ const defaultProps = {
selectedOptions: [],
headerMessage: '',
canSelectMultipleOptions: false,
+ highlightSelectedOptions: false,
hideSectionHeaders: false,
boldStyle: false,
showTitleTooltip: false,
@@ -136,6 +146,8 @@ const defaultProps = {
shouldShowTextInput: true,
onChangeText: () => {},
shouldUseStyleForChildren: true,
+ isRowMultilineSupported: false,
+ initialFocusedIndex: undefined,
};
export {propTypes, defaultProps};
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index d26ad48430b0..7629a1acc0a6 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -652,12 +652,8 @@ function getCategoryOptionTree(options, isOneLine = false) {
/**
* Build the section list for categories
*
- * @param {Object[]} categories
- * @param {String} categories[].name
- * @param {Boolean} categories[].enabled
- * @param {Object[]} recentlyUsedCategories
- * @param {String} recentlyUsedCategories[].name
- * @param {Boolean} recentlyUsedCategories[].enabled
+ * @param {Object} categories
+ * @param {String[]} recentlyUsedCategories
* @param {Object[]} selectedOptions
* @param {String} selectedOptions[].name
* @param {String} searchInputValue
@@ -666,11 +662,12 @@ function getCategoryOptionTree(options, isOneLine = false) {
*/
function getCategoryListSections(categories, recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow) {
const categorySections = [];
- const numberOfCategories = _.size(categories);
+ const categoriesValues = _.values(categories);
+ const numberOfCategories = _.size(categoriesValues);
let indexOffset = 0;
if (!_.isEmpty(searchInputValue)) {
- const searchCategories = _.filter(categories, (category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase()));
+ const searchCategories = _.filter(categoriesValues, (category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase()));
categorySections.push({
// "Search" section
@@ -689,15 +686,21 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt
title: '',
shouldShow: false,
indexOffset,
- data: getCategoryOptionTree(categories),
+ data: getCategoryOptionTree(categoriesValues),
});
return categorySections;
}
const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name);
- const filteredRecentlyUsedCategories = _.filter(recentlyUsedCategories, (category) => !_.includes(selectedOptionNames, category.name));
- const filteredCategories = _.filter(categories, (category) => !_.includes(selectedOptionNames, category.name));
+ const filteredRecentlyUsedCategories = _.map(
+ _.filter(recentlyUsedCategories, (category) => !_.includes(selectedOptionNames, category)),
+ (category) => ({
+ name: category,
+ enabled: lodashGet(categories, `${category}.enabled`, false),
+ }),
+ );
+ const filteredCategories = _.filter(categoriesValues, (category) => !_.includes(selectedOptionNames, category.name));
if (!_.isEmpty(selectedOptions)) {
categorySections.push({
@@ -776,7 +779,7 @@ function getOptions(
},
) {
if (includeCategories) {
- const categoryOptions = getCategoryListSections(_.values(categories), recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow);
+ const categoryOptions = getCategoryListSections(categories, recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow);
return {
recentReports: [],
@@ -1127,7 +1130,7 @@ function getIOUConfirmationOptionsFromParticipants(participants, amountText) {
* @param {boolean} [includeP2P]
* @param {boolean} [includeCategories]
* @param {Object} [categories]
- * @param {Array