Skip to content

Commit d4dd40a

Browse files
authored
Merge pull request Expensify#65823 from Burhan-Rashid/fix/64624
2 parents a6d5615 + cda490a commit d4dd40a

7 files changed

Lines changed: 60 additions & 20 deletions

File tree

src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
2020
import {calculateAmount} from '@libs/IOUUtils';
2121
import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils';
2222
import Parser from '@libs/Parser';
23-
import {getCleanedTagName} from '@libs/PolicyUtils';
23+
import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils';
2424
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
2525
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
2626
import type {TransactionDetails} from '@libs/ReportUtils';
@@ -345,7 +345,7 @@ function TransactionPreviewContent({
345345
numberOfLines={1}
346346
style={[isDeleted && styles.lineThrough, styles.textMicroSupporting, styles.pre, styles.flexShrink1]}
347347
>
348-
{getCleanedTagName(tag)}
348+
{getCommaSeparatedTagNameWithSanitizedColons(tag)}
349349
</Text>
350350
</View>
351351
)}

src/components/TransactionItemRow/TransactionItemRowRBRWithOnyx.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import Icon from '@components/Icon';
55
import {DotIndicator} from '@components/Icon/Expensicons';
66
import RenderHTML from '@components/RenderHTML';
77
import useLocalize from '@hooks/useLocalize';
8+
import useOnyx from '@hooks/useOnyx';
89
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
910
import useTheme from '@hooks/useTheme';
1011
import useThemeStyles from '@hooks/useThemeStyles';
1112
import useTransactionViolations from '@hooks/useTransactionViolations';
1213
import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
1314
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
1415
import variables from '@styles/variables';
16+
import ONYXKEYS from '@src/ONYXKEYS';
1517
import type Transaction from '@src/types/onyx/Transaction';
1618

1719
type TransactionItemRowRBRProps = {
@@ -30,12 +32,12 @@ function TransactionItemRowRBRWithOnyx({transaction, containerStyles, missingFie
3032
const transactionViolations = useTransactionViolations(transaction?.transactionID, false);
3133
const {translate} = useLocalize();
3234
const theme = useTheme();
33-
34-
const {sortedAllReportActions: transactionActions} = usePaginatedReportActions(transaction.reportID);
35+
const {sortedAllReportActions: transactionActions, report} = usePaginatedReportActions(transaction.reportID);
36+
const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true});
3537
const transactionThreadId = transactionActions ? getIOUActionForTransactionID(transactionActions, transaction.transactionID)?.childReportID : undefined;
3638
const {sortedAllReportActions: transactionThreadActions} = usePaginatedReportActions(transactionThreadId);
3739

38-
const RBRMessages = ViolationsUtils.getRBRMessages(transaction, transactionViolations, translate, missingFieldError, transactionThreadActions);
40+
const RBRMessages = ViolationsUtils.getRBRMessages(transaction, transactionViolations, translate, missingFieldError, transactionThreadActions, policyTags);
3941

4042
return (
4143
RBRMessages.length > 0 && (

src/hooks/usePaginatedReportActions.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {useMemo} from 'react';
22
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
33
import PaginationUtils from '@libs/PaginationUtils';
4-
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
5-
import * as ReportUtils from '@libs/ReportUtils';
4+
import {getSortedReportActionsForDisplay} from '@libs/ReportActionsUtils';
5+
import {canUserPerformWriteAction} from '@libs/ReportUtils';
66
import ONYXKEYS from '@src/ONYXKEYS';
77
import useOnyx from './useOnyx';
88

@@ -11,14 +11,15 @@ import useOnyx from './useOnyx';
1111
*/
1212
function usePaginatedReportActions(reportID: string | undefined, reportActionID?: string) {
1313
const nonEmptyStringReportID = getNonEmptyStringOnyxID(reportID);
14-
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${nonEmptyStringReportID}`);
15-
const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report);
14+
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${nonEmptyStringReportID}`, {canBeMissing: true});
15+
const hasWriteAccess = canUserPerformWriteAction(report);
1616

1717
const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${nonEmptyStringReportID}`, {
1818
canEvict: false,
19-
selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, canUserPerformWriteAction, true),
19+
selector: (allReportActions) => getSortedReportActionsForDisplay(allReportActions, hasWriteAccess, true),
20+
canBeMissing: true,
2021
});
21-
const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${nonEmptyStringReportID}`);
22+
const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${nonEmptyStringReportID}`, {canBeMissing: true});
2223

2324
const {
2425
data: reportActions,
@@ -42,6 +43,7 @@ function usePaginatedReportActions(reportID: string | undefined, reportActionID?
4243
sortedAllReportActions,
4344
hasOlderActions: hasNextPage,
4445
hasNewerActions: hasPreviousPage,
46+
report,
4547
};
4648
}
4749

src/libs/PolicyUtils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {translateLocal} from './Localize';
4141
import Navigation from './Navigation/Navigation';
4242
import {isOffline as isOfflineNetworkStore} from './Network/NetworkStore';
4343
import {getAccountIDsByLogins, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils';
44-
import {getAllSortedTransactions, getCategory, getTag} from './TransactionUtils';
44+
import {getAllSortedTransactions, getCategory, getTag, getTagArrayFromName} from './TransactionUtils';
4545
import {isPublicDomain} from './ValidationUtils';
4646

4747
type MemberEmailsToAccountIDs = Record<string, number>;
@@ -467,6 +467,16 @@ function getCleanedTagName(tag: string) {
467467
return tag?.replace(/\\:/g, CONST.COLON);
468468
}
469469

470+
/**
471+
* Converts a colon-delimited tag string into a comma-separated string, filtering out empty tags.
472+
*/
473+
function getCommaSeparatedTagNameWithSanitizedColons(tag: string): string {
474+
return getTagArrayFromName(tag)
475+
.filter((tagItem) => tagItem !== '')
476+
.map(getCleanedTagName)
477+
.join(', ');
478+
}
479+
470480
/**
471481
* Escape colon from tag name
472482
*/
@@ -1488,6 +1498,7 @@ export {
14881498
getPerDiemCustomUnits,
14891499
getAdminEmployees,
14901500
getCleanedTagName,
1501+
getCommaSeparatedTagNameWithSanitizedColons,
14911502
getConnectedIntegration,
14921503
getValidConnectedIntegration,
14931504
getCountOfEnabledTagsOfList,

src/libs/TransactionPreviewUtils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,21 @@ type TranslationPathOrText = {
124124

125125
const dotSeparator: TranslationPathOrText = {text: ` ${CONST.DOT_SEPARATOR} `};
126126

127+
function getMultiLevelTagViolationsCount(violations: OnyxTypes.TransactionViolations): number {
128+
return violations?.reduce((acc, violation) => {
129+
if (violation.type === CONST.VIOLATION_TYPES.VIOLATION && violation.name === CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED) {
130+
const violationCount = violation?.data?.errorIndexes?.length ?? 0;
131+
return acc + violationCount;
132+
}
133+
return acc;
134+
}, 0);
135+
}
136+
127137
function getViolationTranslatePath(violations: OnyxTypes.TransactionViolations, hasFieldErrors: boolean, violationMessage: string, isTransactionOnHold: boolean): TranslationPathOrText {
128138
const violationsCount = violations?.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length ?? 0;
129-
139+
const tagViolationsCount = getMultiLevelTagViolationsCount(violations) ?? 0;
130140
const hasViolationsAndHold = violationsCount > 0 && isTransactionOnHold;
131-
const isTooLong = violationsCount > 1 || violationMessage.length > CONST.REPORT_VIOLATIONS.RBR_MESSAGE_MAX_CHARACTERS_FOR_PREVIEW;
141+
const isTooLong = violationsCount > 1 || tagViolationsCount > 1 || violationMessage.length > CONST.REPORT_VIOLATIONS.RBR_MESSAGE_MAX_CHARACTERS_FOR_PREVIEW;
132142
const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors;
133143

134144
return isTooLong || hasViolationsAndHold || hasViolationsAndFieldErrors ? {translationPath: 'violations.reviewRequired'} : {text: violationMessage};

src/libs/TransactionUtils/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {translateLocal} from '@libs/Localize';
1919
import {rand64, roundToTwoDecimalPlaces} from '@libs/NumberUtils';
2020
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
2121
import {
22-
getCleanedTagName,
22+
getCommaSeparatedTagNameWithSanitizedColons,
2323
getDistanceRateCustomUnitRate,
2424
getPolicy,
2525
getTaxByID,
@@ -837,7 +837,7 @@ function getTag(transaction: OnyxInputOrEntry<Transaction>, tagIndex?: number):
837837
}
838838

839839
function getTagForDisplay(transaction: OnyxEntry<Transaction>, tagIndex?: number): string {
840-
return getCleanedTagName(getTag(transaction, tagIndex));
840+
return getCommaSeparatedTagNameWithSanitizedColons(getTag(transaction, tagIndex));
841841
}
842842

843843
function getCreated(transaction: OnyxInputOrEntry<Transaction>): string {

src/libs/Violations/ViolationsUtils.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import isEmpty from 'lodash/isEmpty';
2+
import keyBy from 'lodash/keyBy';
13
import reject from 'lodash/reject';
24
import Onyx from 'react-native-onyx';
35
import type {OnyxUpdate} from 'react-native-onyx';
@@ -162,6 +164,17 @@ function getTagViolationsForMultiLevelTags(
162164
return getTagViolationForIndependentTags(policyTagList, filteredTransactionViolations, updatedTransaction);
163165
}
164166

167+
/**
168+
* Returns a period-separated string of violation messages for missing tag levels in a multi-level tag, based on error indexes.
169+
*/
170+
function getTagViolationMessagesForMultiLevelTags(tagName: string, errorIndexes: number[], tags: PolicyTagLists, translate: LocaleContextProps['translate']): string {
171+
if (isEmpty(errorIndexes) || isEmpty(tags)) {
172+
return translate('violations.someTagLevelsRequired', {tagName});
173+
}
174+
const tagsWithIndexes = keyBy(Object.values(tags), 'orderWeight');
175+
return errorIndexes.map((i) => translate('violations.someTagLevelsRequired', {tagName: tagsWithIndexes[i]?.name})).join('. ');
176+
}
177+
165178
/**
166179
* Extracts unique error messages from errors and actions
167180
*/
@@ -392,7 +405,7 @@ const ViolationsUtils = {
392405
* possible values could be either translation keys that resolve to strings or translation keys that resolve to
393406
* functions.
394407
*/
395-
getViolationTranslation(violation: TransactionViolation, translate: LocaleContextProps['translate'], canEdit = true): string {
408+
getViolationTranslation(violation: TransactionViolation, translate: LocaleContextProps['translate'], canEdit = true, tags?: PolicyTagLists): string {
396409
const {
397410
brokenBankConnection = false,
398411
isAdmin = false,
@@ -406,11 +419,12 @@ const ViolationsUtils = {
406419
surcharge = 0,
407420
invoiceMarkup = 0,
408421
maxAge = 0,
409-
tagName,
422+
tagName = '',
410423
taxName,
411424
type,
412425
rterType,
413426
message = '',
427+
errorIndexes = [],
414428
} = violation.data ?? {};
415429

416430
switch (violation.name) {
@@ -481,7 +495,7 @@ const ViolationsUtils = {
481495
case 'smartscanFailed':
482496
return translate('violations.smartscanFailed', {canEdit});
483497
case 'someTagLevelsRequired':
484-
return translate('violations.someTagLevelsRequired', {tagName});
498+
return getTagViolationMessagesForMultiLevelTags(tagName, errorIndexes, tags ?? {}, translate);
485499
case 'tagOutOfPolicy':
486500
return translate('violations.tagOutOfPolicy', {tagName});
487501
case 'taxAmountChanged':
@@ -520,6 +534,7 @@ const ViolationsUtils = {
520534
translate: LocaleContextProps['translate'],
521535
missingFieldError?: string,
522536
transactionThreadActions?: ReportAction[],
537+
tags?: PolicyTagLists,
523538
): string {
524539
const errorMessages = extractErrorMessages(transaction?.errors ?? {}, transactionThreadActions?.filter((e) => !!e.errors) ?? [], translate);
525540

@@ -529,7 +544,7 @@ const ViolationsUtils = {
529544
// Some violations end with a period already so lets make sure the connected messages have only single period between them
530545
// and end with a single dot.
531546
...transactionViolations.map((violation) => {
532-
const message = ViolationsUtils.getViolationTranslation(violation, translate);
547+
const message = ViolationsUtils.getViolationTranslation(violation, translate, true, tags);
533548
if (!message) {
534549
return;
535550
}

0 commit comments

Comments
 (0)