Skip to content

[Better Expense Report View] Deleting second to last expense & [Not BERV] concierge message fix + #61680 follow-up #61777

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

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import Log from '@libs/Log';
import {selectAllTransactionsForReport, shouldDisplayReportTableView} from '@libs/MoneyRequestReportUtils';
import navigationRef from '@libs/Navigation/navigationRef';
import {getOneTransactionThreadReportID, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getOneTransactionThreadReportID, isDeletedParentAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {canEditReportAction, getReportOfflinePendingActionAndErrors, isReportTransactionThread} from '@libs/ReportUtils';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import Navigation from '@navigation/Navigation';
Expand Down Expand Up @@ -95,7 +95,8 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe
const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {initialValue: false, canBeMissing: true});
const {reportPendingAction, reportErrors} = getReportOfflinePendingActionAndErrors(report);

const {reportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID);
const {reportActions: reportActionsWithDeletedExpenses, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID);
const reportActions = reportActionsWithDeletedExpenses.filter((value) => !isDeletedParentAction(value));
const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], isOffline);

const [transactions = []] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import {getCleanedTagName} from '@libs/PolicyUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import {getOriginalMessage, getReportActions, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import type {TransactionDetails} from '@libs/ReportUtils';
import {canEditMoneyRequest, getTransactionDetails, getWorkspaceIcon, isIOUReport, isPolicyExpenseChat, isReportApproved, isSettled} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
Expand Down Expand Up @@ -60,6 +60,7 @@ function TransactionPreviewContent({
const ownerAccountID = iouReport?.ownerAccountID ?? reportPreviewAction?.childOwnerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const isReportAPolicyExpenseChat = isPolicyExpenseChat(chatReport);
const {amount: requestAmount, comment: requestComment, merchant, tag, category, currency: requestCurrency} = transactionDetails;
const reportActions = useMemo(() => (iouReport ? getReportActions(iouReport) ?? {} : {}), [iouReport]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldnt it be cleaner to user something like
const [reportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {canBeMissing: false}); with a selector?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont wanna block on this as the pr brings valuable improvements but I think useOnyx with selector is more App like solution


const transactionPreviewCommonArguments = useMemo(
() => ({
Expand Down Expand Up @@ -96,8 +97,9 @@ function TransactionPreviewContent({
...transactionPreviewCommonArguments,
shouldShowRBR,
violationMessage,
reportActions,
}),
[transactionPreviewCommonArguments, shouldShowRBR, violationMessage],
[transactionPreviewCommonArguments, shouldShowRBR, violationMessage, reportActions],
);
const getTranslatedText = (item: TranslationPathOrText) => (item.translationPath ? translate(item.translationPath) : item.text ?? '');

Expand Down
14 changes: 7 additions & 7 deletions src/libs/TransactionPreviewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {convertToDisplayString} from './CurrencyUtils';
import DateUtils from './DateUtils';
import type {PlatformStackRouteProp} from './Navigation/PlatformStackNavigation/types';
import type {TransactionDuplicateNavigatorParamList} from './Navigation/types';
import {getOriginalMessage, getReportAction, getReportActions, isMessageDeleted, isMoneyRequestAction} from './ReportActionsUtils';
import {getOriginalMessage, getReportAction, isMessageDeleted, isMoneyRequestAction} from './ReportActionsUtils';
import {hasActionsWithErrors, hasReportViolations, isPaidGroupPolicy, isPaidGroupPolicyExpenseReport, isReportApproved, isReportOwner, isSettled} from './ReportUtils';
import type {TransactionDetails} from './ReportUtils';
import StringUtils from './StringUtils';
Expand Down Expand Up @@ -140,10 +140,8 @@ function getViolationTranslatePath(violations: OnyxTypes.TransactionViolations,
* it returns an empty array. It identifies the latest error in each action and filters out duplicates to
* ensure only unique error messages are returned.
*/
function getUniqueActionErrors(report: OnyxEntry<OnyxTypes.Report>) {
const reportActions = Object.values(report ? getReportActions(report) ?? {} : {});

const reportErrors = reportActions.map((reportAction) => {
function getUniqueActionErrors(reportActions: OnyxTypes.ReportActions) {
const reportErrors = Object.values(reportActions).map((reportAction) => {
const errors = reportAction.errors ?? {};
const key = Object.keys(errors).sort().reverse().at(0) ?? '';
const error = errors[key];
Expand All @@ -162,6 +160,7 @@ function getTransactionPreviewTextAndTranslationPaths({
isBillSplit,
shouldShowRBR,
violationMessage,
reportActions,
}: {
iouReport: OnyxEntry<OnyxTypes.Report>;
transaction: OnyxEntry<OnyxTypes.Transaction>;
Expand All @@ -171,6 +170,7 @@ function getTransactionPreviewTextAndTranslationPaths({
isBillSplit: boolean;
shouldShowRBR: boolean;
violationMessage?: string;
reportActions?: OnyxTypes.ReportActions;
}) {
const isFetchingWaypoints = isFetchingWaypointsFromServer(transaction);
const isTransactionOnHold = isOnHold(transaction);
Expand Down Expand Up @@ -216,8 +216,8 @@ function getTransactionPreviewTextAndTranslationPaths({
}
}

if (RBRMessage === undefined && hasActionWithErrors) {
const actionsWithErrors = getUniqueActionErrors(iouReport);
if (RBRMessage === undefined && hasActionWithErrors && !!reportActions) {
const actionsWithErrors = getUniqueActionErrors(reportActions);
RBRMessage = actionsWithErrors.length > 1 ? {translationPath: 'violations.reviewRequired'} : {text: actionsWithErrors.at(0)};
}

Expand Down
10 changes: 6 additions & 4 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7111,21 +7111,23 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT
// STEP 2: Decide if we need to:
// 1. Delete the transactionThread - delete if there are no visible comments in the thread
// 2. Update the moneyRequestPreview to show [Deleted expense] - update if the transactionThread exists AND it isn't being deleted
const shouldDeleteTransactionThread = transactionThreadID ? (reportAction?.childVisibleActionCount ?? 0) === 0 : false;
const shouldShowDeletedRequestMessage = !!transactionThreadID && !shouldDeleteTransactionThread;
// The current state is that we want to get rid of the [Deleted expense] breadcrumb,
// so we never want to display it if transactionThreadID is present.
const shouldDeleteTransactionThread = !!transactionThreadID;

// STEP 3: Update the IOU reportAction and decide if the iouReport should be deleted. We delete the iouReport if there are no visible comments left in the report.
const updatedReportAction = {
[reportAction.reportActionID]: {
pendingAction: shouldShowDeletedRequestMessage ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
previousMessage: reportAction.message,
message: [
{
type: 'COMMENT',
html: '',
text: '',
isEdited: true,
isDeletedParentAction: shouldShowDeletedRequestMessage,
// We need this here to filter out deleted actions
isDeletedParentAction: shouldDeleteTransactionThread,
},
],
originalMessage: {
Expand Down
9 changes: 8 additions & 1 deletion src/pages/home/ReportScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.accountID, canBeMissing: false});
const [currentUserEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (value) => value?.email, canBeMissing: false});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true});
const {reportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute);
const {
reportActions: reportActionsWithDeletedExpenses,
linkedAction,
sortedAllReportActions,
hasNewerActions,
hasOlderActions,
} = usePaginatedReportActions(reportID, reportActionIDFromRoute);
const reportActions = reportActionsWithDeletedExpenses.filter((value) => !isDeletedParentAction(value));

const [isBannerVisible, setIsBannerVisible] = useState(true);
const [scrollPosition, setScrollPosition] = useState<ScrollPosition>({});
Expand Down
23 changes: 6 additions & 17 deletions tests/unit/TransactionPreviewUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import {buildOptimisticIOUReport, buildOptimisticIOUReportAction} from '@libs/Re
import {createTransactionPreviewConditionals, getTransactionPreviewTextAndTranslationPaths, getUniqueActionErrors, getViolationTranslatePath} from '@libs/TransactionPreviewUtils';
import {buildOptimisticTransaction} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import * as ReportActionUtils from '@src/libs/ReportActionsUtils';
import * as ReportUtils from '@src/libs/ReportUtils';
import type {Report, ReportActions} from '@src/types/onyx';
import {iouReportR14932 as mockedReport} from '../../__mocks__/reportData/reports';
import type {ReportActions} from '@src/types/onyx';

const basicProps = {
iouReport: buildOptimisticIOUReport(123, 234, 1000, '1', 'USD'),
Expand Down Expand Up @@ -258,14 +256,8 @@ describe('TransactionPreviewUtils', () => {
});

describe('getUniqueActionErrors', () => {
test('returns an empty array if there is no report or it is empty', () => {
expect(getUniqueActionErrors(undefined)).toEqual([]);
expect(getUniqueActionErrors({} as Report)).toEqual([]);
});

test('returns an empty array if there are no actions in the report', () => {
jest.spyOn(ReportActionUtils, 'getReportActions').mockReturnValue({});
expect(getUniqueActionErrors(mockedReport)).toEqual([]);
test('returns an empty array if there are no actions', () => {
expect(getUniqueActionErrors({})).toEqual([]);
});

test('returns unique error messages from report actions', () => {
Expand All @@ -276,10 +268,9 @@ describe('TransactionPreviewUtils', () => {
3: {errors: {a: 'Error A', d: 'Error D'}},
/* eslint-enable @typescript-eslint/naming-convention */
} as unknown as ReportActions;
jest.spyOn(ReportActionUtils, 'getReportActions').mockReturnValue(actions);

const expectedErrors = ['Error B', 'Error C', 'Error D'];
expect(getUniqueActionErrors(mockedReport).sort()).toEqual(expectedErrors.sort());
expect(getUniqueActionErrors(actions).sort()).toEqual(expectedErrors.sort());
});

test('returns the latest error message if multiple errors exist under a single action', () => {
Expand All @@ -288,9 +279,8 @@ describe('TransactionPreviewUtils', () => {
1: {errors: {z: 'Error Z2', a: 'Error A', f: 'Error Z'}},
/* eslint-enable @typescript-eslint/naming-convention */
} as unknown as ReportActions;
jest.spyOn(ReportActionUtils, 'getReportActions').mockReturnValue(actions);

expect(getUniqueActionErrors(mockedReport)).toEqual(['Error Z2']);
expect(getUniqueActionErrors(actions)).toEqual(['Error Z2']);
});

test('filters out non-string error messages', () => {
Expand All @@ -300,9 +290,8 @@ describe('TransactionPreviewUtils', () => {
2: {errors: {c: null, d: 'Error D'}},
/* eslint-enable @typescript-eslint/naming-convention */
} as unknown as ReportActions;
jest.spyOn(ReportActionUtils, 'getReportActions').mockReturnValue(actions);

expect(getUniqueActionErrors(mockedReport)).toEqual(['Error B', 'Error D']);
expect(getUniqueActionErrors(actions)).toEqual(['Error B', 'Error D']);
});
});
});
Loading