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

#26793: Edit a tag in a money request #27950

Merged
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,7 @@ const CONST = {
MERCHANT: 'merchant',
CATEGORY: 'category',
RECEIPT: 'receipt',
TAG: 'tag',
},
FOOTER: {
EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`,
Expand Down
11 changes: 6 additions & 5 deletions src/components/MoneyRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import transactionPropTypes from './transactionPropTypes';
import DistanceRequestUtils from '../libs/DistanceRequestUtils';
import * as IOU from '../libs/actions/IOU';
import * as TransactionUtils from '../libs/TransactionUtils';
import * as PolicyUtils from '../libs/PolicyUtils';

const propTypes = {
/** Callback to inform parent modal of success */
Expand Down Expand Up @@ -202,12 +203,12 @@ function MoneyRequestConfirmationList(props) {
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 policyTag = PolicyUtils.getTag(props.policyTags);
const policyTagList = lodashGet(policyTag, 'tags', {});
const policyTagListName = lodashGet(policyTag, 'name', translate('common.tag'));
const canUseTags = Permissions.canUseTags(props.betas);
// A flag for showing the tags field
const shouldShowTags = isPolicyExpenseChat && canUseTags && _.any(tagList, (tag) => tag.enabled);
const shouldShowTags = isPolicyExpenseChat && canUseTags && OptionsListUtils.hasEnabledOptions(_.values(policyTagList));

// A flag for showing the billable field
const shouldShowBillable = canUseTags && !lodashGet(props.policy, 'disabledFields.defaultBillable', true);
Expand Down Expand Up @@ -541,7 +542,7 @@ function MoneyRequestConfirmationList(props) {
<MenuItemWithTopDescription
shouldShowRightIcon={!props.isReadOnly}
title={props.iouTag}
description={tagListName || translate('common.tag')}
description={policyTagListName}
onPress={() => Navigation.navigate(ROUTES.getMoneyRequestTagRoute(props.iouType, props.reportID))}
style={[styles.moneyRequestMenuItem, styles.mb2]}
disabled={didConfirm || props.isReadOnly}
Expand Down
38 changes: 36 additions & 2 deletions src/components/ReportActionItem/MoneyRequestView.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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 * as PolicyUtils from '../../libs/PolicyUtils';
import CONST from '../../CONST';
import * as Expensicons from '../Icon/Expensicons';
import iouReportPropTypes from '../../pages/iouReportPropTypes';
Expand All @@ -32,6 +33,7 @@ import * as TransactionUtils from '../../libs/TransactionUtils';
import OfflineWithFeedback from '../OfflineWithFeedback';
import categoryPropTypes from '../categoryPropTypes';
import SpacerView from '../SpacerView';
import tagPropTypes from '../tagPropTypes';

const propTypes = {
/** The report currently being looked at */
Expand All @@ -53,6 +55,15 @@ const propTypes = {
/** The transaction associated with the transactionThread */
transaction: transactionPropTypes,

/** Collection of tags attached to a policy */
policyTags: PropTypes.objectOf(
PropTypes.shape({
name: PropTypes.string,
required: PropTypes.bool,
tags: PropTypes.objectOf(tagPropTypes),
}),
),

...withCurrentUserPersonalDetailsPropTypes,
};

Expand All @@ -65,9 +76,10 @@ const defaultProps = {
currency: CONST.CURRENCY.USD,
comment: {comment: ''},
},
policyTags: {},
};

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

Expand All @@ -80,6 +92,7 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should
comment: transactionDescription,
merchant: transactionMerchant,
category: transactionCategory,
tag: transactionTag,
} = ReportUtils.getTransactionDetails(transaction);
const isEmptyMerchant =
transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
Expand All @@ -89,8 +102,14 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should
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

// Fetches only the first tag, for now
const policyTag = PolicyUtils.getTag(policyTags);
const policyTagsList = lodashGet(policyTag, 'tags', {});

// Flags for showing categories and tags
const shouldShowCategory = isPolicyExpenseChat && Permissions.canUseCategories(betas) && (transactionCategory || OptionsListUtils.hasEnabledOptions(lodashValues(policyCategories)));
const shouldShowTag = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList)));

let description = `${translate('iou.amount')} • ${translate('iou.cash')}`;
if (isSettled) {
Expand Down Expand Up @@ -200,6 +219,18 @@ function MoneyRequestView({betas, report, parentReport, policyCategories, should
/>
</OfflineWithFeedback>
)}
{shouldShowTag && (
<OfflineWithFeedback pendingAction={lodashGet(transaction, 'pendingFields.category') || lodashGet(transaction, 'pendingAction')}>
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
<MenuItemWithTopDescription
description={lodashGet(policyTag, 'name') || translate('common.tag')}
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
title={transactionTag}
interactive={canEdit}
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAG))}
/>
</OfflineWithFeedback>
)}
<SpacerView
shouldShow={shouldShowHorizontalRule}
style={[shouldShowHorizontalRule ? styles.reportHorizontalRule : {}]}
Expand Down Expand Up @@ -237,5 +268,8 @@ export default compose(
return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
},
},
policyTags: {
key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`,
},
}),
)(MoneyRequestView);
3 changes: 2 additions & 1 deletion src/components/TagPicker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ONYXKEYS from '../../ONYXKEYS';
import styles from '../../styles/styles';
import useLocalize from '../../hooks/useLocalize';
import * as OptionsListUtils from '../../libs/OptionsListUtils';
import * as PolicyUtils from '../../libs/PolicyUtils';
import OptionsSelector from '../OptionsSelector';
import {propTypes, defaultProps} from './tagPickerPropTypes';

Expand All @@ -15,7 +16,7 @@ function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, onSubm
const [searchValue, setSearchValue] = useState('');

const policyRecentlyUsedTagsList = lodashGet(policyRecentlyUsedTags, tag, []);
const policyTagList = lodashGet(policyTags, [tag, 'tags'], {});
const policyTagList = PolicyUtils.getTagList(policyTags, tag);
const policyTagsCount = _.size(_.filter(policyTagList, (policyTag) => policyTag.enabled));
const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD;

Expand Down
53 changes: 53 additions & 0 deletions src/libs/PolicyUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,56 @@ function getIneligibleInvitees(policyMembers, personalDetails) {
return memberEmailsToExclude;
}

/**
* Fetches the tag from policy tags, defaults to the first if no key is provided.
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
*
* @param {Object} policyTags
* @param {String} [tagKey]
* @returns {String}
*/
function getTag(policyTags, tagKey) {
if (!policyTags) {
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
return '';
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
}

const policyTagKey = tagKey || _.first(_.keys(policyTags));

return lodashGet(policyTags, policyTagKey);
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Fetches the first tag name from policy tags.
*
* @param {Object} policyTags
* @returns {String}
*/
function getTagListName(policyTags) {
if (!policyTags) {
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
return '';
}

const policyTagKeys = _.keys(policyTags) || [];

return lodashGet(policyTags, [_.first(policyTagKeys), 'name'], '');
}

/**
* Fetches the tags of a policy for a specific key. Defaults to the first tag if no key is provided.
*
* @param {Object} policyTags
* @param {String} [tagKey]
* @returns {String}
*/
function getTagList(policyTags, tagKey) {
if (!policyTags) {
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
return {};
}

const policyTagKey = tagKey || _.first(_.keys(policyTags));

return lodashGet(policyTags, [policyTagKey, 'tags'], {});
}

export {
getActivePolicies,
hasPolicyMemberError,
Expand All @@ -179,4 +229,7 @@ export {
isPolicyAdmin,
getMemberAccountIDsForWorkspace,
getIneligibleInvitees,
getTag,
getTagListName,
getTagList,
};
17 changes: 17 additions & 0 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,7 @@ function getTransactionDetails(transaction) {
comment: TransactionUtils.getDescription(transaction),
merchant: TransactionUtils.getMerchant(transaction),
category: TransactionUtils.getCategory(transaction),
tag: TransactionUtils.getTag(transaction),
};
}

Expand Down Expand Up @@ -1580,6 +1581,16 @@ function getModifiedExpenseMessage(reportAction) {
if (hasModifiedCategory) {
return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage.category, reportActionOriginalMessage.oldCategory, Localize.translateLocal('common.category'), true);
}

const hasModifiedTag = _.has(reportActionOriginalMessage, 'oldTag') && _.has(reportActionOriginalMessage, 'tag');
if (hasModifiedTag) {
return getProperSchemaForModifiedExpenseMessage(
reportActionOriginalMessage.tag,
reportActionOriginalMessage.oldTag,
reportActionOriginalMessage.tagListName || Localize.translateLocal('common.tag'),
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
true,
);
}
}

/**
Expand Down Expand Up @@ -1625,6 +1636,12 @@ function getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, i
originalMessage.category = transactionChanges.category;
}

if (_.has(transactionChanges, 'tag')) {
originalMessage.oldTag = TransactionUtils.getTag(oldTransaction);
originalMessage.tag = transactionChanges.tag;
originalMessage.tagListName = transactionChanges.tagListName;
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
}

return originalMessage;
}

Expand Down
16 changes: 16 additions & 0 deletions src/libs/TransactionUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseRep
updatedTransaction.category = transactionChanges.category;
}

if (_.has(transactionChanges, 'tag')) {
updatedTransaction.tag = transactionChanges.tag;
}

if (shouldStopSmartscan && _.has(transaction, 'receipt') && !_.isEmpty(transaction.receipt) && lodashGet(transaction, 'receipt.state') !== CONST.IOU.RECEIPT_STATE.OPEN) {
updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN;
}
Expand All @@ -162,6 +166,7 @@ function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseRep
...(_.has(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(_.has(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(_.has(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(_.has(transactionChanges, 'tag') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
BeeMargarida marked this conversation as resolved.
Show resolved Hide resolved
};

return updatedTransaction;
Expand Down Expand Up @@ -253,6 +258,16 @@ function getCategory(transaction) {
return lodashGet(transaction, 'category', '');
}

/**
* Return the tag from the transaction. This "tag" field has no "modified" complement.
*
* @param {Object} transaction
* @return {String}
*/
function getTag(transaction) {
return lodashGet(transaction, 'tag', '');
}

/**
* Return the created field from the transaction, return the modifiedCreated if present.
*
Expand Down Expand Up @@ -399,6 +414,7 @@ export {
getMerchant,
getCreated,
getCategory,
getTag,
getLinkedTransaction,
getAllReportTransactions,
hasReceipt,
Expand Down
23 changes: 22 additions & 1 deletion src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,17 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC
updatedChatReport.lastMessageHtml = messageText;
}

const optimisticPolicyRecentlyUsedTags = {};
if (_.has(transactionChanges, 'tag')) {
const tagListName = transactionChanges.tagListName;
const recentlyUsedPolicyTags = allRecentlyUsedTags[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`];

if (recentlyUsedPolicyTags) {
const uniquePolicyRecentlyUsedTags = _.filter(recentlyUsedPolicyTags[tagListName], (recentlyUsedPolicyTag) => recentlyUsedPolicyTag !== transactionChanges.tag);
optimisticPolicyRecentlyUsedTags[tagListName] = [transactionChanges.tag, ...uniquePolicyRecentlyUsedTags];
}
}

// STEP 4: Compose the optimistic data
const optimisticData = [
{
Expand All @@ -1173,6 +1184,14 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC
},
];

if (!_.isEmpty(optimisticPolicyRecentlyUsedTags)) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`,
value: optimisticPolicyRecentlyUsedTags,
});
}

const successData = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -1192,6 +1211,7 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC
currency: null,
merchant: null,
category: null,
tag: null,
},
},
},
Expand Down Expand Up @@ -1230,7 +1250,7 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC
];

// STEP 6: Call the API endpoint
const {created, amount, currency, comment, merchant, category} = ReportUtils.getTransactionDetails(updatedTransaction);
const {created, amount, currency, comment, merchant, category, tag} = ReportUtils.getTransactionDetails(updatedTransaction);
API.write(
'EditMoneyRequest',
{
Expand All @@ -1242,6 +1262,7 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC
comment,
merchant,
category,
tag,
},
{optimisticData, successData, failureData},
);
Expand Down
Loading