Skip to content

Commit 0f104f2

Browse files
abzokhattabMelvinBot
authored andcommitted
Decode tag in TagCell so tag column matches group-by-tag dropdown
Addresses review feedback on PR Expensify#85603: - cead22: tag column showed raw HTML-encoded value (e.g. `uno & dos`) while the group-by-tag dropdown decoded it. TagCell now decodes the display value, mirroring how CategoryCell already handles categories. - github-actions CONSISTENCY-3: extract `getDecodedTagName` into TagUtils (mirroring `getDecodedCategoryName` in CategoryUtils) and use it from ReportLayoutUtils instead of importing `Str` directly. Keeps the tag / category decoding pattern symmetric across the codebase. Adds unit tests for `getDecodedTagName`.
1 parent 965e5c7 commit 0f104f2

4 files changed

Lines changed: 39 additions & 7 deletions

File tree

src/components/TransactionItemRow/DataCells/TagCell.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,28 @@ import TextWithIconCell from '@components/Search/SearchList/ListItem/TextWithIco
33
import TextWithTooltip from '@components/TextWithTooltip';
44
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
55
import useThemeStyles from '@hooks/useThemeStyles';
6+
import {getDecodedTagName} from '@libs/TagUtils';
67
import {getTagForDisplay} from '@libs/TransactionUtils';
78
import type TransactionDataCellProps from './TransactionDataCellProps';
89

910
function TagCell({shouldUseNarrowLayout, shouldShowTooltip, transactionItem}: TransactionDataCellProps) {
1011
const icons = useMemoizedLazyExpensifyIcons(['Tag']);
1112
const styles = useThemeStyles();
13+
// Decode HTML entities so tags stored with encoding (e.g. `uno & dos`) display as `uno & dos`,
14+
// matching the report's group-by-tag dropdown which already decodes the value.
15+
const tagForDisplay = getDecodedTagName(getTagForDisplay(transactionItem));
16+
1217
return shouldUseNarrowLayout ? (
1318
<TextWithIconCell
1419
icon={icons.Tag}
1520
showTooltip={shouldShowTooltip}
16-
text={getTagForDisplay(transactionItem)}
21+
text={tagForDisplay}
1722
textStyle={[styles.textMicro, styles.mnh0]}
1823
/>
1924
) : (
2025
<TextWithTooltip
2126
shouldShowTooltip={shouldShowTooltip}
22-
text={getTagForDisplay(transactionItem)}
27+
text={tagForDisplay}
2328
numberOfLines={1}
2429
style={[styles.lineHeightLarge, styles.justifyContentCenter]}
2530
/>

src/libs/ReportLayoutUtils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import {Str} from 'expensify-common';
21
import type {OnyxEntry} from 'react-native-onyx';
32
import type {LocaleContextProps} from '@components/LocaleContextProvider';
43
import type {GroupedTransactions} from '@src/types/onyx';
54
import type Report from '@src/types/onyx/Report';
65
import type Transaction from '@src/types/onyx/Transaction';
76
import {getDecodedCategoryName, isCategoryMissing} from './CategoryUtils';
8-
import {isTagMissing} from './TagUtils';
7+
import {getDecodedTagName, isTagMissing} from './TagUtils';
98
import {getAmount, getCategory, getCurrency, getTag, isTransactionPendingDelete} from './TransactionUtils';
109

1110
/**
@@ -100,7 +99,7 @@ function groupTransactionsByTag(transactions: Transaction[], report: OnyxEntry<R
10099

101100
for (const transaction of transactions) {
102101
const tag = getTag(transaction);
103-
const tagKey = isTagMissing(tag) ? '' : Str.htmlDecode(tag);
102+
const tagKey = isTagMissing(tag) ? '' : getDecodedTagName(tag);
104103

105104
if (!groups.has(tagKey)) {
106105
groups.set(tagKey, []);

src/libs/TagUtils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {Str} from 'expensify-common';
12
import CONST from '@src/CONST';
23

34
/**
@@ -19,4 +20,12 @@ function trimTag(tag: string): string {
1920
return tagWithoutEscapedColons.replace(/:*$/, '').replaceAll('☢', '\\:');
2021
}
2122

22-
export {isTagMissing, trimTag};
23+
/**
24+
* HTML-decodes a tag name so values stored with different encodings (e.g. `R&amp;D` vs `R&D`)
25+
* resolve to the same string. Mirrors getDecodedCategoryName in CategoryUtils.
26+
*/
27+
function getDecodedTagName(tagName: string): string {
28+
return Str.htmlDecode(tagName);
29+
}
30+
31+
export {isTagMissing, trimTag, getDecodedTagName};

tests/unit/TagUtilsTest.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {isTagMissing, trimTag} from '@libs/TagUtils';
1+
import {getDecodedTagName, isTagMissing, trimTag} from '@libs/TagUtils';
22
import CONST from '@src/CONST';
33

44
describe('TagUtils', () => {
@@ -68,4 +68,23 @@ describe('TagUtils', () => {
6868
expect(trimTag('tag\\:name\\\\::')).toBe('tag\\:name\\\\:');
6969
});
7070
});
71+
72+
describe('getDecodedTagName', () => {
73+
it('decodes &amp; to &', () => {
74+
expect(getDecodedTagName('R&amp;D')).toBe('R&D');
75+
});
76+
77+
it('returns an unencoded string unchanged', () => {
78+
expect(getDecodedTagName('R&D')).toBe('R&D');
79+
});
80+
81+
it('returns an empty string when input is empty', () => {
82+
expect(getDecodedTagName('')).toBe('');
83+
});
84+
85+
it('decodes other common HTML entities', () => {
86+
expect(getDecodedTagName('a &lt; b &gt; c')).toBe('a < b > c');
87+
expect(getDecodedTagName('&quot;hello&quot;')).toBe('"hello"');
88+
});
89+
});
7190
});

0 commit comments

Comments
 (0)