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

Allow Viewing/Editing a category on a Money Request #27459

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
32d1355
delegate category selection to parent
rezkiy37 Sep 14, 2023
d799686
implement edit request category page
rezkiy37 Sep 14, 2023
da95354
preview and navigate to edit a category
rezkiy37 Sep 14, 2023
bfc59ba
prepare api
rezkiy37 Sep 14, 2023
ca8edb1
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Sep 15, 2023
05f020c
do not stop smart scan
rezkiy37 Sep 15, 2023
636924f
handle a selected category alone
rezkiy37 Sep 15, 2023
e13de7f
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Sep 18, 2023
812ea05
do not use modified category
rezkiy37 Sep 18, 2023
3dd7b5c
create getRootParentReport helper
rezkiy37 Sep 18, 2023
d7b6b89
improve conditions of MoneyRequestConfirmationList
rezkiy37 Sep 18, 2023
ddb21c8
improve conditions of MoneyRequestView
rezkiy37 Sep 18, 2023
f8b84a9
improve options helpers
rezkiy37 Sep 18, 2023
b12a93e
check enabled categories
rezkiy37 Sep 18, 2023
9c1815d
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Sep 18, 2023
7a643bf
fix billable condition
rezkiy37 Sep 18, 2023
a4c07d8
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Sep 19, 2023
d9dfaa7
sort prop types
rezkiy37 Sep 19, 2023
4f875ea
simplify helpers
rezkiy37 Sep 19, 2023
d962ce1
create a handler
rezkiy37 Sep 19, 2023
72bd7e8
reuse iuo props
rezkiy37 Sep 19, 2023
37d6770
clarify props
rezkiy37 Sep 19, 2023
3353869
clarify return type
rezkiy37 Sep 19, 2023
70b44d8
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Sep 20, 2023
abd2d25
re-test
rezkiy37 Sep 20, 2023
8912f26
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Sep 20, 2023
5cdc7fb
clarify comment
rezkiy37 Sep 20, 2023
ce2907d
perform conditions
rezkiy37 Sep 20, 2023
96ad0fc
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Sep 21, 2023
d42958e
Merge branch 'main' of https://github.com/rezkiy37/Expensify into fea…
rezkiy37 Sep 21, 2023
8753fa3
update name of a method
rezkiy37 Sep 21, 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
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);
yuwenmemon marked this conversation as resolved.
Show resolved Hide resolved
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
rezkiy37 marked this conversation as resolved.
Show resolved Hide resolved
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)
rezkiy37 marked this conversation as resolved.
Show resolved Hide resolved
.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
Loading