Skip to content

Commit

Permalink
Merge pull request #29101 from rezkiy37/fix/28963-edit-category-paid-…
Browse files Browse the repository at this point in the history
…request

Improve conditions for allowing/disallowing editing a money request
  • Loading branch information
mountiny authored Oct 23, 2023
2 parents f42a195 + fe7e3d3 commit 22c19f1
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 52 deletions.
13 changes: 7 additions & 6 deletions src/components/ReportActionItem/MoneyRequestView.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should
const isExpensifyCardTransaction = TransactionUtils.isExpensifyCardTransaction(transaction);
const cardProgramName = isExpensifyCardTransaction ? CardUtils.getCardDescription(transactionCardID) : '';

// Flags for allowing or disallowing editing a money request
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
const canEdit = ReportUtils.canEditMoneyRequest(parentReportAction) && !isExpensifyCardTransaction;

Expand Down Expand Up @@ -181,8 +182,8 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should
titleIcon={Expensicons.Checkmark}
description={amountDescription}
titleStyle={styles.newKansasLarge}
interactive={canEdit}
shouldShowRightIcon={canEdit}
interactive={canEdit && !isSettled}
shouldShowRightIcon={canEdit && !isSettled}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))}
brickRoadIndicator={hasErrors && transactionAmount === 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
error={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''}
Expand All @@ -206,8 +207,8 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should
<MenuItemWithTopDescription
description={translate('common.distance')}
title={transactionMerchant}
interactive={canEdit}
shouldShowRightIcon={canEdit}
interactive={canEdit && !isSettled}
shouldShowRightIcon={canEdit && !isSettled}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))}
/>
Expand All @@ -230,8 +231,8 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should
<MenuItemWithTopDescription
description={translate('common.date')}
title={transactionDate}
interactive={canEdit}
shouldShowRightIcon={canEdit}
interactive={canEdit && !isSettled}
shouldShowRightIcon={canEdit && !isSettled}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
brickRoadIndicator={hasErrors && transactionDate === '' ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
Expand Down
51 changes: 47 additions & 4 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1487,28 +1487,70 @@ function getTransactionDetails(transaction, createdDateFormat = CONST.DATE.FNS_F
* Can only edit if:
*
* - in case of IOU report
* - the current user is the requestor
* - the current user is the requestor and is not settled yet
* - in case of expense report
* - the current user is the requestor
* - the current user is the requestor and is not settled yet
* - or the user is an admin on the policy the expense report is tied to
*
* @param {Object} reportAction
* @returns {Boolean}
*/
function canEditMoneyRequest(reportAction) {
// If the report action i snot IOU type, return true early
const isDeleted = ReportActionsUtils.isDeletedAction(reportAction);

if (isDeleted) {
return false;
}

// If the report action is not IOU type, return true early
if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) {
return true;
}

const moneyRequestReportID = lodashGet(reportAction, 'originalMessage.IOUReportID', 0);

if (!moneyRequestReportID) {
return false;
}

const moneyRequestReport = getReport(moneyRequestReportID);
const isReportSettled = isSettled(moneyRequestReport.reportID);
const isAdmin = isExpenseReport(moneyRequestReport) && lodashGet(getPolicy(moneyRequestReport.policyID), 'role', '') === CONST.POLICY.ROLE.ADMIN;
const isRequestor = currentUserAccountID === reportAction.actorAccountID;
return !isReportSettled && (isAdmin || isRequestor);

if (isAdmin) {
return true;
}

return !isReportSettled && isRequestor;
}

/**
* Checks if the current user can edit the provided property of a money request
*
* @param {Object} reportAction
* @param {String} reportID
* @param {String} fieldToEdit
* @returns {Boolean}
*/
function canEditFieldOfMoneyRequest(reportAction, reportID, fieldToEdit) {
// A list of fields that cannot be edited by anyone, once a money request has been settled
const nonEditableFieldsWhenSettled = [
CONST.EDIT_REQUEST_FIELD.AMOUNT,
CONST.EDIT_REQUEST_FIELD.CURRENCY,
CONST.EDIT_REQUEST_FIELD.DATE,
CONST.EDIT_REQUEST_FIELD.RECEIPT,
CONST.EDIT_REQUEST_FIELD.DISTANCE,
];

// Checks if this user has permissions to edit this money request
if (!canEditMoneyRequest(reportAction)) {
return false; // User doesn't have permission to edit
}

// Checks if the report is settled
// Checks if the provided property is a restricted one
return !isSettled(reportID) || !nonEditableFieldsWhenSettled.includes(fieldToEdit);
}

/**
Expand Down Expand Up @@ -4124,6 +4166,7 @@ export {
getTaskAssigneeChatOnyxData,
getParticipantsIDs,
canEditMoneyRequest,
canEditFieldOfMoneyRequest,
buildTransactionThread,
areAllRequestsBeingSmartScanned,
getTransactionsWithReceipts,
Expand Down
55 changes: 14 additions & 41 deletions src/pages/EditRequestPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@ import ONYXKEYS from '../ONYXKEYS';
import ROUTES from '../ROUTES';
import compose from '../libs/compose';
import Navigation from '../libs/Navigation/Navigation';
import * as ReportActionsUtils from '../libs/ReportActionsUtils';
import * as ReportUtils from '../libs/ReportUtils';
import * as PolicyUtils from '../libs/PolicyUtils';
import * as TransactionUtils from '../libs/TransactionUtils';
import * as Policy from '../libs/actions/Policy';
import * as IOU from '../libs/actions/IOU';
import * as CurrencyUtils from '../libs/CurrencyUtils';
import * as OptionsListUtils from '../libs/OptionsListUtils';
import Permissions from '../libs/Permissions';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '../components/withCurrentUserPersonalDetails';
import tagPropTypes from '../components/tagPropTypes';
import FullPageNotFoundView from '../components/BlockingViews/FullPageNotFoundView';
import EditRequestDescriptionPage from './EditRequestDescriptionPage';
Expand All @@ -31,6 +28,7 @@ import EditRequestCategoryPage from './EditRequestCategoryPage';
import EditRequestTagPage from './EditRequestTagPage';
import categoryPropTypes from '../components/categoryPropTypes';
import ScreenWrapper from '../components/ScreenWrapper';
import reportActionPropTypes from './home/report/reportActionPropTypes';
import transactionPropTypes from '../components/transactionPropTypes';

const propTypes = {
Expand All @@ -56,49 +54,32 @@ const propTypes = {
/** The parent report object for the thread report */
parentReport: reportPropTypes,

/** The policy object for the current route */
policy: PropTypes.shape({
/** The name of the policy */
name: PropTypes.string,

/** The URL for the policy avatar */
avatar: PropTypes.string,
}),

/** Session info for the currently logged in user. */
session: PropTypes.shape({
/** Currently logged in user email */
email: PropTypes.string,
}),

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

/** Collection of tags attached to a policy */
policyTags: tagPropTypes,

/** The original transaction that is being edited */
transaction: transactionPropTypes,
/** The actions from the parent report */
parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),

...withCurrentUserPersonalDetailsPropTypes,
/** Transaction that stores the request data */
transaction: transactionPropTypes,
};

const defaultProps = {
betas: [],
report: {},
parentReport: {},
policy: null,
session: {
email: null,
},
policyCategories: {},
policyTags: {},
parentReportActions: {},
transaction: {},
};

function EditRequestPage({betas, report, route, parentReport, policy, session, policyCategories, policyTags, parentReportActions, transaction}) {
function EditRequestPage({betas, report, route, parentReport, policyCategories, policyTags, parentReportActions, transaction}) {
const parentReportActionID = lodashGet(report, 'parentReportActionID', '0');
const parentReportAction = lodashGet(parentReportActions, parentReportActionID);
const parentReportAction = lodashGet(parentReportActions, parentReportActionID, {});
const {
amount: transactionAmount,
currency: transactionCurrency,
Expand All @@ -114,13 +95,6 @@ function EditRequestPage({betas, report, route, parentReport, policy, session, p
const transactionCreated = TransactionUtils.getCreated(transaction);
const fieldToEdit = lodashGet(route, ['params', 'field'], '');

const isDeleted = ReportActionsUtils.isDeletedAction(parentReportAction);
const isSettled = ReportUtils.isSettled(parentReport.reportID);

const isAdmin = Policy.isAdminOfFreePolicy([policy]) && ReportUtils.isExpenseReport(parentReport);
const isRequestor = ReportUtils.isMoneyRequestReport(parentReport) && lodashGet(session, 'accountID', null) === parentReportAction.actorAccountID;
const canEdit = !isSettled && !isDeleted && (isAdmin || isRequestor);

// For now, it always defaults to the first tag of the policy
const policyTag = PolicyUtils.getTag(policyTags);
const policyTagList = lodashGet(policyTag, 'tags', {});
Expand All @@ -135,15 +109,18 @@ function EditRequestPage({betas, report, route, parentReport, policy, session, p
// A flag for showing the tags page
const shouldShowTags = isPolicyExpenseChat && Permissions.canUseTags(betas) && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagList)));

// Dismiss the modal when the request is paid or deleted
// Decides whether to allow or disallow editing a money request
useEffect(() => {
if (canEdit) {
// Do not dismiss the modal, when a current user can edit this property of the money request.
if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, parentReport.reportID, fieldToEdit)) {
return;
}

// Dismiss the modal when a current user cannot edit a money request.
Navigation.isNavigationReady().then(() => {
Navigation.dismissModal();
});
}, [canEdit]);
}, [parentReportAction, parentReport.reportID, fieldToEdit]);

// Update the transaction object and close the modal
function editMoneyRequest(transactionChanges) {
Expand Down Expand Up @@ -300,7 +277,6 @@ EditRequestPage.displayName = 'EditRequestPage';
EditRequestPage.propTypes = propTypes;
EditRequestPage.defaultProps = defaultProps;
export default compose(
withCurrentUserPersonalDetails,
withOnyx({
betas: {
key: ONYXKEYS.BETAS,
Expand All @@ -311,9 +287,6 @@ export default compose(
}),
// eslint-disable-next-line rulesdir/no-multiple-onyx-in-file
withOnyx({
policy: {
key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`,
},
policyCategories: {
key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`,
},
Expand Down
26 changes: 25 additions & 1 deletion src/pages/iou/IOUCurrencySelection.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState, useMemo, useCallback, useRef} from 'react';
import React, {useState, useMemo, useCallback, useRef, useEffect} from 'react';
import {Keyboard} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
Expand All @@ -14,6 +14,8 @@ import compose from '../../libs/compose';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import {withNetwork} from '../../components/OnyxProvider';
import * as CurrencyUtils from '../../libs/CurrencyUtils';
import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
import * as ReportUtils from '../../libs/ReportUtils';
import ROUTES from '../../ROUTES';
import {iouPropTypes, iouDefaultProps} from './propTypes';
import SelectionList from '../../components/SelectionList';
Expand Down Expand Up @@ -71,6 +73,28 @@ function IOUCurrencySelection(props) {
const selectedCurrencyCode = (lodashGet(props.route, 'params.currency', props.iou.currency) || CONST.CURRENCY.USD).toUpperCase();
const iouType = lodashGet(props.route, 'params.iouType', CONST.IOU.TYPE.REQUEST);
const reportID = lodashGet(props.route, 'params.reportID', '');
const threadReportID = lodashGet(props.route, 'params.threadReportID', '');

// Decides whether to allow or disallow editing a money request
useEffect(() => {
// Do not dismiss the modal, when it is not the edit flow.
if (!threadReportID) {
return;
}

const report = ReportUtils.getReport(threadReportID);
const parentReportAction = ReportActionsUtils.getReportAction(report.parentReportID, report.parentReportActionID);

// Do not dismiss the modal, when a current user can edit this currency of this money request.
if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, report.parentReportID, CONST.EDIT_REQUEST_FIELD.CURRENCY)) {
return;
}

// Dismiss the modal when a current user cannot edit a money request.
Navigation.isNavigationReady().then(() => {
Navigation.dismissModal();
});
}, [threadReportID]);

const confirmCurrencySelection = useCallback(
(option) => {
Expand Down

0 comments on commit 22c19f1

Please sign in to comment.