Skip to content

Commit d8bbc0a

Browse files
authored
Merge pull request #74928 from whiletrace/dev#68721
implemented HTML parsing for getReportNames()/ Conditional Parsing for Displaynames
2 parents 257dc63 + 56dd3af commit d8bbc0a

File tree

7 files changed

+159
-9
lines changed

7 files changed

+159
-9
lines changed

src/components/DisplayNames/index.native.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import TextWithEmojiFragment from '@pages/home/report/comment/TextWithEmojiFragm
88
import type DisplayNamesProps from './types';
99

1010
// As we don't have to show tooltips of the Native platform so we simply render the full display names list.
11-
function DisplayNames({accessibilityLabel, fullTitle, textStyles = [], numberOfLines = 1, renderAdditionalText, forwardedFSClass, testID}: DisplayNamesProps) {
11+
function DisplayNames({accessibilityLabel, fullTitle, textStyles = [], numberOfLines = 1, renderAdditionalText, forwardedFSClass, testID, shouldParseHtml = false}: DisplayNamesProps) {
1212
const {translate} = useLocalize();
13+
const title = shouldParseHtml
14+
? StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden')
15+
: StringUtils.lineBreaksToSpaces(fullTitle) || translate('common.hidden');
1316
const titleContainsTextAndCustomEmoji = useMemo(() => containsCustomEmoji(fullTitle) && !containsOnlyCustomEmoji(fullTitle), [fullTitle]);
1417
return (
1518
<Text
@@ -21,11 +24,11 @@ function DisplayNames({accessibilityLabel, fullTitle, textStyles = [], numberOfL
2124
>
2225
{titleContainsTextAndCustomEmoji ? (
2326
<TextWithEmojiFragment
24-
message={StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden')}
27+
message={title}
2528
style={textStyles}
2629
/>
2730
) : (
28-
StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden')
31+
title
2932
)}
3033
{renderAdditionalText?.()}
3134
</Text>

src/components/DisplayNames/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ function DisplayNames({
1616
displayNamesWithTooltips,
1717
renderAdditionalText,
1818
forwardedFSClass,
19+
shouldParseHtml = false,
1920
}: DisplayNamesProps) {
2021
const {translate} = useLocalize();
21-
const title = StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden');
22+
const title = shouldParseHtml
23+
? StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden')
24+
: StringUtils.lineBreaksToSpaces(fullTitle) || translate('common.hidden');
2225

2326
if (!tooltipEnabled) {
2427
return (

src/components/DisplayNames/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ type DisplayNamesProps = ForwardedFSClassProps & {
4949

5050
/** TestID indicating order */
5151
testID?: number;
52+
53+
/** Whether to parse HTML in the fullTitle */
54+
shouldParseHtml?: boolean;
5255
};
5356

5457
export default DisplayNamesProps;

src/libs/ReportUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5704,7 +5704,7 @@ function getReportName(
57045704
}
57055705

57065706
if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION)) {
5707-
return getUnreportedTransactionMessage();
5707+
return Parser.htmlToText(getUnreportedTransactionMessage());
57085708
}
57095709

57105710
if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT)) {

src/pages/ReportDetailsPage.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import ConfirmModal from '@components/ConfirmModal';
1010
import DisplayNames from '@components/DisplayNames';
1111
import HeaderWithBackButton from '@components/HeaderWithBackButton';
1212
import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
13-
import * as Expensicons from '@components/Icon/Expensicons';
1413
import MenuItem from '@components/MenuItem';
1514
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
1615
import OfflineWithFeedback from '@components/OfflineWithFeedback';
@@ -27,6 +26,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
2726
import useDeleteTransactions from '@hooks/useDeleteTransactions';
2827
import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations';
2928
import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction';
29+
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
3030
import useLocalize from '@hooks/useLocalize';
3131
import useNetwork from '@hooks/useNetwork';
3232
import useOnyx from '@hooks/useOnyx';
@@ -41,7 +41,6 @@ import Navigation from '@libs/Navigation/Navigation';
4141
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
4242
import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types';
4343
import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils';
44-
import Parser from '@libs/Parser';
4544
import Permissions from '@libs/Permissions';
4645
import {isPolicyAdmin as isPolicyAdminUtil, isPolicyEmployee as isPolicyEmployeeUtil, shouldShowPolicy} from '@libs/PolicyUtils';
4746
import {getOneTransactionThreadReportID, getOriginalMessage, getTrackExpenseActionableWhisper, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
@@ -149,6 +148,7 @@ const CASES = {
149148
type CaseID = ValueOf<typeof CASES>;
150149

151150
function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetailsPageProps) {
151+
const Expensicons = useMemoizedLazyExpensifyIcons(['Bug', 'Building', 'Camera', 'Checkmark', 'Exit', 'Folder', 'Gear', 'Pencil', 'Send', 'Trashcan', 'UserPlus', 'Users'] as const);
152152
const {translate, localeCompare} = useLocalize();
153153
const {isOffline} = useNetwork();
154154
const {isRestrictedToPreferredPolicy, preferredPolicyID} = usePreferredPolicy();
@@ -332,8 +332,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail
332332

333333
const shouldShowLeaveButton = canLeaveChat(report, policy, !!reportNameValuePairs?.private_isArchived);
334334
const shouldShowGoToWorkspace = shouldShowPolicy(policy, false, currentUserPersonalDetails?.email) && !policy?.isJoinRequestPending;
335-
336-
const reportName = Parser.htmlToText(getReportName(report, undefined, undefined, undefined, undefined, reportAttributes));
335+
const reportName = getReportName(report, undefined, undefined, undefined, undefined, reportAttributes);
337336

338337
const additionalRoomDetails =
339338
(isPolicyExpenseChat && !!report?.isOwnPolicyExpenseChat) || isExpenseReportUtil(report) || isPolicyExpenseChat || isInvoiceRoom
@@ -578,6 +577,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail
578577
isRestrictedToPreferredPolicy,
579578
preferredPolicyID,
580579
introSelected,
580+
Expensicons,
581581
]);
582582

583583
const displayNamesWithTooltips = useMemo(() => {
@@ -660,6 +660,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail
660660
policy,
661661
participants,
662662
moneyRequestReport?.reportID,
663+
Expensicons,
663664
]);
664665

665666
const canJoin = canJoinChat(report, parentReportAction, policy, !!reportNameValuePairs?.private_isArchived);
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {render, screen} from '@testing-library/react-native';
2+
import React from 'react';
3+
import DisplayNames from '@components/DisplayNames';
4+
5+
jest.mock('@hooks/useLocalize', () => ({
6+
// eslint-disable-next-line @typescript-eslint/naming-convention
7+
__esModule: true,
8+
default: () => ({
9+
translate: jest.fn((key: string): string => {
10+
if (key === 'common.hidden') {
11+
return 'hidden';
12+
}
13+
return key;
14+
}),
15+
}),
16+
}));
17+
18+
jest.mock('@libs/Parser', () => ({
19+
// eslint-disable-next-line @typescript-eslint/naming-convention
20+
__esModule: true,
21+
default: {
22+
htmlToText: jest.fn((html: string) => {
23+
// Simulate stripTag behavior: remove anything that looks like HTML tags
24+
return html.replace(/(<([^>]+)>)/gi, '');
25+
}),
26+
},
27+
}));
28+
29+
jest.mock('@libs/StringUtils', () => ({
30+
// eslint-disable-next-line @typescript-eslint/naming-convention
31+
__esModule: true,
32+
default: {
33+
lineBreaksToSpaces: jest.fn((text: string) => text),
34+
},
35+
}));
36+
37+
describe('DisplayNames - shouldParseHtml prop', () => {
38+
const testTitle = '< >';
39+
40+
afterEach(() => {
41+
jest.clearAllMocks();
42+
});
43+
44+
it('should NOT parse HTML by default (shouldParseHtml defaults to false)', () => {
45+
render(
46+
<DisplayNames
47+
fullTitle={testTitle}
48+
numberOfLines={1}
49+
tooltipEnabled={false}
50+
/>,
51+
);
52+
53+
// With shouldParseHtml = false (default), "< >" should be preserved
54+
expect(screen.getByText('< >')).toBeTruthy();
55+
});
56+
57+
it('should parse HTML when shouldParseHtml is explicitly set to true', () => {
58+
const htmlTitle = '<b>Bold Text</b>';
59+
render(
60+
<DisplayNames
61+
fullTitle={htmlTitle}
62+
numberOfLines={1}
63+
tooltipEnabled={false}
64+
shouldParseHtml
65+
/>,
66+
);
67+
68+
// With shouldParseHtml = true, HTML tags should be stripped
69+
expect(screen.getByText('Bold Text')).toBeTruthy();
70+
expect(screen.queryByText('<b>Bold Text</b>')).toBeNull();
71+
});
72+
73+
it('should preserve special characters when shouldParseHtml is false', () => {
74+
const specialCharsTitle = '< > & " \' test';
75+
render(
76+
<DisplayNames
77+
fullTitle={specialCharsTitle}
78+
numberOfLines={1}
79+
tooltipEnabled={false}
80+
/>,
81+
);
82+
83+
// Special characters should be preserved when not parsing HTML
84+
expect(screen.getByText(specialCharsTitle)).toBeTruthy();
85+
});
86+
87+
it('should show "hidden" when title is empty and HTML parsing is disabled', () => {
88+
render(
89+
<DisplayNames
90+
fullTitle=""
91+
numberOfLines={1}
92+
tooltipEnabled={false}
93+
/>,
94+
);
95+
96+
expect(screen.getByText('hidden')).toBeTruthy();
97+
});
98+
99+
it('should show "hidden" when title becomes empty after HTML parsing', () => {
100+
const onlyTagsTitle = '<div></div>';
101+
render(
102+
<DisplayNames
103+
fullTitle={onlyTagsTitle}
104+
numberOfLines={1}
105+
tooltipEnabled={false}
106+
shouldParseHtml
107+
/>,
108+
);
109+
110+
// After parsing, only tags remain which get stripped to empty string
111+
expect(screen.getByText('hidden')).toBeTruthy();
112+
});
113+
});

tests/unit/ReportUtilsTest.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,33 @@ describe('ReportUtils', () => {
11231123
);
11241124
});
11251125
});
1126+
1127+
describe('Unreported transaction thread', () => {
1128+
test('HTML is stripped from unreported transaction message', () => {
1129+
const transactionThread = {
1130+
...LHNTestUtils.getFakeReport(),
1131+
type: CONST.REPORT.TYPE.CHAT,
1132+
reportID: '123',
1133+
parentReportID: '456',
1134+
};
1135+
1136+
const unreportedTransactionAction = {
1137+
actionName: CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION,
1138+
originalMessage: {
1139+
fromReportID: '789',
1140+
},
1141+
} as ReportAction;
1142+
1143+
const reportName = getReportName(transactionThread, undefined, unreportedTransactionAction);
1144+
1145+
// Should NOT contain HTML tags
1146+
expect(reportName).not.toContain('<a href');
1147+
expect(reportName).not.toContain('</a>');
1148+
// Should contain the text content
1149+
expect(reportName).toContain('moved this expense');
1150+
expect(reportName).toContain('personal space');
1151+
});
1152+
});
11261153
});
11271154

11281155
// Need to merge the same tests

0 commit comments

Comments
 (0)