Conversation
🦜 Polyglot Parrot! 🦜Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues: View the translation diffdiff --git a/src/languages/de.ts b/src/languages/de.ts
index 82c27cb3..232fbd8a 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -6941,7 +6941,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
[CONST.SEARCH.GROUP_BY.CARD]: 'Karte',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'Auszahlungs-ID',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Kategorie',
- [CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
+ [CONST.SEARCH.GROUP_BY.TAG]: 'Stichwort',
},
feed: 'Feed',
withdrawalType: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 1867d96d..9825ef3f 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -6929,9 +6929,9 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
groupBy: {
[CONST.SEARCH.GROUP_BY.FROM]: 'Da',
[CONST.SEARCH.GROUP_BY.CARD]: 'Carta',
- [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID prelievo',
+ [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID prelievo', //_/\__/_/ \_,_/\__/\__/\_,_/
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categoria',
- [CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
+ [CONST.SEARCH.GROUP_BY.TAG]: 'Etichetta',
},
feed: 'Feed',
withdrawalType: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index ad9a5160..f3176d60 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -6867,10 +6867,10 @@ ${reportName}
reimbursable: '精算対象',
purchaseCurrency: '購入通貨',
groupBy: {
- [CONST.SEARCH.GROUP_BY.FROM]: '差出人',
+ [CONST.SEARCH.GROUP_BY.FROM]: '送信者',
[CONST.SEARCH.GROUP_BY.CARD]: 'カード',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: '出金ID',
- [CONST.SEARCH.GROUP_BY.CATEGORY]: 'カテゴリー',
+ [CONST.SEARCH.GROUP_BY.CATEGORY]: 'カテゴリ',
[CONST.SEARCH.GROUP_BY.TAG]: 'タグ',
},
feed: 'フィード',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index ea256103..6c1225ce 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -6913,7 +6913,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten
[CONST.SEARCH.GROUP_BY.CARD]: 'Kaart',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'Opname-ID',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categorie',
- [CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
+ [CONST.SEARCH.GROUP_BY.TAG]: 'Label',
},
feed: 'Feed',
withdrawalType: {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 0482d4d8..9fa7fdbe 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -6901,7 +6901,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
[CONST.SEARCH.GROUP_BY.CARD]: 'Karta',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID wypłaty',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Kategoria',
- [CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
+ [CONST.SEARCH.GROUP_BY.TAG]: 'Etykieta',
},
feed: 'Kanał',
withdrawalType: {
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 802e60af..ebd23761 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -6901,9 +6901,9 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe
groupBy: {
[CONST.SEARCH.GROUP_BY.FROM]: 'De',
[CONST.SEARCH.GROUP_BY.CARD]: 'Cartão',
- [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID de saque',
+ [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID da retirada',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categoria',
- [CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
+ [CONST.SEARCH.GROUP_BY.TAG]: 'Etiqueta',
},
feed: 'Feed',
withdrawalType: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index c8b835b4..91c7e6ea 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -6748,9 +6748,10 @@ ${reportName}
reimbursable: '可报销',
purchaseCurrency: '购买货币',
groupBy: {
- [CONST.SEARCH.GROUP_BY.FROM]: '从',
+ [CONST.SEARCH.GROUP_BY.FROM]: '来自',
[CONST.SEARCH.GROUP_BY.CARD]: '卡片',
- [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: '提款 ID',
+ //_/\__/_/ \_,_/\__/\__/\_,_/
+ [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: '提现 ID',
[CONST.SEARCH.GROUP_BY.CATEGORY]: '类别',
[CONST.SEARCH.GROUP_BY.TAG]: '标签',
},
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
|
@ahmedGaber93 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
| } | ||
|
|
||
| if (isTransactionTagGroupListItemType(item)) { | ||
| const tagValue = item.tag === '' || item.tag === '(untagged)' ? CONST.SEARCH.TAG_EMPTY_VALUE : item.tag; |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
This tag value normalization logic is duplicated in src/libs/SearchUIUtils.ts (in the getTagSections function). The same logic appears twice:
const tagValue = item.tag === '' || item.tag === '(untagged)' ? CONST.SEARCH.TAG_EMPTY_VALUE : item.tag;Fix: Extract this logic into a shared utility function:
// In SearchUIUtils.ts or a shared utilities file
function normalizeTagValue(tag: string): string {
return tag === '' || tag === '(untagged)' ? CONST.SEARCH.TAG_EMPTY_VALUE : tag;
}
// Then use it in both places:
const tagValue = normalizeTagValue(item.tag);Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
| columns?: SearchColumnType[]; | ||
| }; | ||
|
|
||
| function TagListItemHeader<TItem extends ListItem>({ |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
The entire TagListItemHeader component is nearly identical to CategoryListItemHeader, with only minor differences (tag vs category names, translation keys, and constants). This creates significant code duplication and maintenance overhead.
Current duplication:
- Same structure and layout (140+ lines)
- Same props interface
- Same hooks and styling logic
- Same JSX structure
- Only differences: property names and specific constants
Fix: Extract the common logic into a shared generic component:
// Create a generic GroupListItemHeader component
function GroupListItemHeader<TItem extends ListItem, TGroupItem>({
groupItem,
displayName,
columnComponents,
// ... other common props
}) {
// Shared implementation for all group headers
}
// Then TagListItemHeader becomes:
function TagListItemHeader(props) {
const displayName = /* tag-specific logic */;
const columnComponents = /* tag-specific columns */;
return <GroupListItemHeader {...props} displayName={displayName} columnComponents={columnComponents} />;
}This follows the DRY principle and makes it easier to maintain consistent behavior across all group header types.
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
src/libs/SearchUIUtils.ts
Outdated
|
|
||
| // Format the tag name - keep empty/none values as-is | ||
| const rawTag = tagGroup.tag; | ||
| const formattedTag = !rawTag || rawTag === CONST.SEARCH.TAG_EMPTY_VALUE ? rawTag : rawTag; |
There was a problem hiding this comment.
❌ CONSISTENCY-2 (docs)
The ternary expression \!rawTag || rawTag === CONST.SEARCH.TAG_EMPTY_VALUE ? rawTag : rawTag is redundant - both branches return rawTag.
Current code:
const formattedTag = \!rawTag || rawTag === CONST.SEARCH.TAG_EMPTY_VALUE ? rawTag : rawTag;Fix: This simplifies to just assigning rawTag directly:
const formattedTag = rawTag;If the intent was to perform some formatting or transformation, the logic is missing. Otherwise, this line serves no purpose and adds confusion.
Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
Reviewer Checklist
Screenshots/Videos |
| ]; | ||
|
|
||
| const tagName1 = 'Project A'; | ||
| const tagName2 = 'Project B'; |
There was a problem hiding this comment.
Can consider adding tests for multi-level tags also.
There was a problem hiding this comment.
Looks good, apart from this bug which might be NAB.
ESLint fixes need to be done.
|
@carlosmiceli Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
trjExpensify
left a comment
There was a problem hiding this comment.
Core PR for a whatsnext project! 👍
00ff34e
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
@neil-marcellini I think GROUP_BY_DEFAULT_SORT and GROUP_BY_DEFAULT_SORT_ORDER should also have been updated in |
|
And also |
|
@jasperhuangg Looks like one of the new commits overwrote the peggy changes. Might have happened while fixing conflicts. |
|
Created #80737 |
|
🚀 Deployed to staging by https://github.com/neil-marcellini in version: 9.3.11-0 🚀
|
|
🚀 Deployed to production by https://github.com/Julesssss in version: 9.3.12-1 🚀
|







Explanation of Change
Similar to this previous PR feat: [Insights] [Release 1] Top Categories - Add group-by:category, add support for grouping expenses by tag.
Fixed Issues
$ #80394
PROPOSAL: N/A
Tests
Set up
Basic test
2026-01-27_10-34-04.mp4
Expenses without tags
2026-01-27_10-42-47.mp4
Verify selection
2026-01-27_10-43-42.mp4
"No tag" sorting
Offline tests
N/A
QA Steps
Same as tests
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps./** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Screenshots/Videos
All changes are platform independent. See videos above for web.
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari