Skip to content

Commit

Permalink
Merge pull request #27459 from rezkiy37/feature/24464-editing-categor…
Browse files Browse the repository at this point in the history
…y-money-request

Allow Viewing/Editing a category on a Money Request
  • Loading branch information
yuwenmemon authored Sep 21, 2023
2 parents f39e93f + 8753fa3 commit 00f77a7
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 136 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,7 @@ const CONST = {
DATE: 'date',
DESCRIPTION: 'description',
MERCHANT: 'merchant',
CATEGORY: 'category',
RECEIPT: 'receipt',
},
FOOTER: {
Expand Down
16 changes: 5 additions & 11 deletions src/components/CategoryPicker/categoryPickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ import PropTypes from 'prop-types';
import categoryPropTypes from '../categoryPropTypes';

const propTypes = {
/** The report ID of the IOU */
reportID: PropTypes.string.isRequired,

/** The policyID we are getting categories for */
policyID: PropTypes.string,

/** The type of IOU report, i.e. bill, request, send */
iouType: PropTypes.string.isRequired,
/** The selected category of an expense */
selectedCategory: PropTypes.string,

/* Onyx Props */
/** Collection of categories attached to a policy */
Expand All @@ -19,18 +16,15 @@ const propTypes = {
/** 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,
}),
/** Callback to fire when a category is pressed */
onSubmit: PropTypes.func.isRequired,
};

const defaultProps = {
policyID: '',
selectedCategory: '',
policyCategories: {},
policyRecentlyUsedCategories: [],
iou: {},
};

export {propTypes, defaultProps};
32 changes: 6 additions & 26 deletions src/components/CategoryPicker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,31 @@ import lodashGet from 'lodash/get';
import ONYXKEYS from '../../ONYXKEYS';
import {propTypes, defaultProps} from './categoryPickerPropTypes';
import styles from '../../styles/styles';
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}) {
function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedCategories, onSubmit}) {
const {translate} = useLocalize();
const [searchValue, setSearchValue] = useState('');

const policyCategoriesCount = _.size(policyCategories);
const policyCategoriesCount = OptionsListUtils.getEnabledCategoriesCount(_.values(policyCategories));
const isCategoriesCountBelowThreshold = policyCategoriesCount < CONST.CATEGORY_LIST_THRESHOLD;

const selectedOptions = useMemo(() => {
if (!iou.category) {
if (!selectedCategory) {
return [];
}

return [
{
name: iou.category,
name: selectedCategory,
enabled: true,
accountID: null,
},
];
}, [iou.category]);
}, [selectedCategory]);

const initialFocusedIndex = useMemo(() => {
if (isCategoriesCountBelowThreshold && selectedOptions.length > 0) {
Expand All @@ -53,20 +50,6 @@ function CategoryPicker({policyCategories, reportID, iouType, iou, policyRecentl
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 (
<OptionsSelector
optionHoveredStyle={styles.hoveredComponentBG}
Expand All @@ -81,7 +64,7 @@ function CategoryPicker({policyCategories, reportID, iouType, iou, policyRecentl
highlightSelectedOptions
isRowMultilineSupported
onChangeText={setSearchValue}
onSelectRow={updateCategory}
onSelectRow={onSubmit}
/>
);
}
Expand All @@ -97,7 +80,4 @@ export default withOnyx({
policyRecentlyUsedCategories: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`,
},
iou: {
key: ONYXKEYS.IOU,
},
})(CategoryPicker);
17 changes: 13 additions & 4 deletions src/components/MoneyRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,23 @@ 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 shouldCategoryBeEditable = !_.isEmpty(props.policyCategories) && Permissions.canUseCategories(props.betas);

// A flag for verifying that the current report is a sub-report of a workspace chat
const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(ReportUtils.getReport(props.reportID))), [props.reportID]);

// A flag for showing the categories field
const shouldShowCategories = isPolicyExpenseChat && Permissions.canUseCategories(props.betas) && OptionsListUtils.hasEnabledOptions(_.values(props.policyCategories));

// Fetches the first tag list of the policy
const tagListKey = _.first(_.keys(props.policyTags));
const tagList = lodashGet(props.policyTags, [tagListKey, 'tags'], []);
const tagListName = lodashGet(props.policyTags, [tagListKey, 'name'], '');
const canUseTags = Permissions.canUseTags(props.betas);
const shouldShowTags = canUseTags && _.any(tagList, (tag) => tag.enabled);
// A flag for showing the tags field
const shouldShowTags = isPolicyExpenseChat && canUseTags && _.any(tagList, (tag) => tag.enabled);

// A flag for showing the billable field
const shouldShowBillable = canUseTags && !lodashGet(props.policy, 'disabledFields.defaultBillable', true);

const hasRoute = TransactionUtils.hasRoute(transaction);
const isDistanceRequestWithoutRoute = props.isDistanceRequest && !hasRoute;
Expand Down Expand Up @@ -518,7 +527,7 @@ function MoneyRequestConfirmationList(props) {
disabled={didConfirm || props.isReadOnly || !isTypeRequest}
/>
)}
{shouldCategoryBeEditable && (
{shouldShowCategories && (
<MenuItemWithTopDescription
shouldShowRightIcon={!props.isReadOnly}
title={props.iouCategory}
Expand All @@ -538,7 +547,7 @@ function MoneyRequestConfirmationList(props) {
disabled={didConfirm || props.isReadOnly}
/>
)}
{canUseTags && !lodashGet(props.policy, 'disabledFields.defaultBillable', true) && (
{shouldShowBillable && (
<View style={[styles.flexRow, styles.mb4, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
<Text color={!props.iouIsBillable ? themeColors.textSupporting : undefined}>{translate('common.billable')}</Text>
<Switch
Expand Down
46 changes: 41 additions & 5 deletions src/components/ReportActionItem/MoneyRequestView.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import React from 'react';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import lodashValues from 'lodash/values';
import PropTypes from 'prop-types';
import reportPropTypes from '../../pages/reportPropTypes';
import ONYXKEYS from '../../ONYXKEYS';
import ROUTES from '../../ROUTES';
import Navigation from '../../libs/Navigation/Navigation';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '../withCurrentUserPersonalDetails';
import compose from '../../libs/compose';
import Permissions from '../../libs/Permissions';
import MenuItemWithTopDescription from '../MenuItemWithTopDescription';
import styles from '../../styles/styles';
import * as ReportUtils from '../../libs/ReportUtils';
import * as OptionsListUtils from '../../libs/OptionsListUtils';
import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
import * as StyleUtils from '../../styles/StyleUtils';
import CONST from '../../CONST';
Expand All @@ -27,34 +30,44 @@ import Image from '../Image';
import ReportActionItemImage from './ReportActionItemImage';
import * as TransactionUtils from '../../libs/TransactionUtils';
import OfflineWithFeedback from '../OfflineWithFeedback';
import categoryPropTypes from '../categoryPropTypes';
import SpacerView from '../SpacerView';

const propTypes = {
/** The report currently being looked at */
report: reportPropTypes.isRequired,

/** Whether we should display the horizontal rule below the component */
shouldShowHorizontalRule: PropTypes.bool.isRequired,

/* Onyx Props */
/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),

/** The expense report or iou report (only will have a value if this is a transaction thread) */
parentReport: iouReportPropTypes,

/** Collection of categories attached to a policy */
policyCategories: PropTypes.objectOf(categoryPropTypes),

/** The transaction associated with the transactionThread */
transaction: transactionPropTypes,

/** Whether we should display the horizontal rule below the component */
shouldShowHorizontalRule: PropTypes.bool.isRequired,

...withCurrentUserPersonalDetailsPropTypes,
};

const defaultProps = {
betas: [],
parentReport: {},
policyCategories: {},
transaction: {
amount: 0,
currency: CONST.CURRENCY.USD,
comment: {comment: ''},
},
};

function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, transaction}) {
function MoneyRequestView({betas, report, parentReport, policyCategories, shouldShowHorizontalRule, transaction}) {
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();

Expand All @@ -66,13 +79,18 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, trans
currency: transactionCurrency,
comment: transactionDescription,
merchant: transactionMerchant,
category: transactionCategory,
} = ReportUtils.getTransactionDetails(transaction);
const isEmptyMerchant =
transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
const formattedTransactionAmount = transactionAmount && transactionCurrency && CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency);

const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
const canEdit = ReportUtils.canEditMoneyRequest(parentReportAction);
// A flag for verifying that the current report is a sub-report of a workspace chat
const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]);
// A flag for showing categories
const shouldShowCategory = isPolicyExpenseChat && Permissions.canUseCategories(betas) && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories)));

let description = `${translate('iou.amount')}${translate('iou.cash')}`;
if (isSettled) {
Expand Down Expand Up @@ -170,6 +188,18 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, trans
subtitleTextStyle={styles.textLabelError}
/>
</OfflineWithFeedback>
{shouldShowCategory && (
<OfflineWithFeedback pendingAction={lodashGet(transaction, 'pendingFields.category') || lodashGet(transaction, 'pendingAction')}>
<MenuItemWithTopDescription
description={translate('common.category')}
title={transactionCategory}
interactive={canEdit}
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.CATEGORY))}
/>
</OfflineWithFeedback>
)}
<SpacerView
shouldShow={shouldShowHorizontalRule}
style={[shouldShowHorizontalRule ? styles.reportHorizontalRule : {}]}
Expand All @@ -185,12 +215,18 @@ MoneyRequestView.displayName = 'MoneyRequestView';
export default compose(
withCurrentUserPersonalDetails,
withOnyx({
betas: {
key: ONYXKEYS.BETAS,
},
parentReport: {
key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`,
},
policy: {
key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`,
},
policyCategories: {
key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report.policyID}`,
},
session: {
key: ONYXKEYS.SESSION,
},
Expand Down
48 changes: 47 additions & 1 deletion src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,30 @@ function isCurrentUser(userDetails) {
return _.some(_.keys(loginList), (login) => login.toLowerCase() === userDetailsLogin.toLowerCase());
}

/**
* Calculates count of all enabled options
*
* @param {Object[]} options - an initial strings array
* @param {Boolean} options[].enabled - a flag to enable/disable option in a list
* @param {String} options[].name - a name of an option
* @returns {Number}
*/
function getEnabledCategoriesCount(options) {
return _.filter(options, (option) => option.enabled).length;
}

/**
* Verifies that there is at least one enabled option
*
* @param {Object[]} options - an initial strings array
* @param {Boolean} options[].enabled - a flag to enable/disable option in a list
* @param {String} options[].name - a name of an option
* @returns {Boolean}
*/
function hasEnabledOptions(options) {
return _.some(options, (option) => option.enabled);
}

/**
* Build the options for the category tree hierarchy via indents
*
Expand All @@ -606,6 +630,10 @@ function getCategoryOptionTree(options, isOneLine = false) {
const optionCollection = {};

_.each(options, (option) => {
if (!option.enabled) {
return;
}

if (isOneLine) {
if (_.has(optionCollection, option.name)) {
return;
Expand Down Expand Up @@ -656,10 +684,26 @@ function getCategoryOptionTree(options, isOneLine = false) {
*/
function getCategoryListSections(categories, recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow) {
const categorySections = [];
const categoriesValues = _.values(categories);
const categoriesValues = _.chain(categories)
.values()
.filter((category) => category.enabled)
.value();

const numberOfCategories = _.size(categoriesValues);
let indexOffset = 0;

if (numberOfCategories === 0 && selectedOptions.length > 0) {
categorySections.push({
// "Selected" section
title: '',
shouldShow: false,
indexOffset,
data: getCategoryOptionTree(selectedOptions, true),
});

return categorySections;
}

if (!_.isEmpty(searchInputValue)) {
const searchCategories = _.filter(categoriesValues, (category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase()));

Expand Down Expand Up @@ -1474,6 +1518,8 @@ export {
isSearchStringMatch,
shouldOptionShowTooltip,
getLastMessageTextForReport,
getEnabledCategoriesCount,
hasEnabledOptions,
getCategoryOptionTree,
formatMemberForList,
};
Loading

0 comments on commit 00f77a7

Please sign in to comment.