Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6869,6 +6869,7 @@ const CONST = {
CARD: 'card',
WITHDRAWAL_ID: 'withdrawal-id',
CATEGORY: 'category',
TAG: 'tag',
MONTH: 'month',
},
get TYPE_CUSTOM_COLUMNS() {
Expand Down Expand Up @@ -6951,6 +6952,11 @@ const CONST = {
EXPENSES: this.TABLE_COLUMNS.GROUP_EXPENSES,
TOTAL: this.TABLE_COLUMNS.GROUP_TOTAL,
},
TAG: {
TAG: this.TABLE_COLUMNS.GROUP_TAG,
EXPENSES: this.TABLE_COLUMNS.GROUP_EXPENSES,
TOTAL: this.TABLE_COLUMNS.GROUP_TOTAL,
},
MONTH: {
MONTH: this.TABLE_COLUMNS.GROUP_MONTH,
EXPENSES: this.TABLE_COLUMNS.GROUP_EXPENSES,
Expand Down Expand Up @@ -6998,6 +7004,7 @@ const CONST = {
this.TABLE_COLUMNS.GROUP_TOTAL,
],
CATEGORY: [this.TABLE_COLUMNS.GROUP_CATEGORY, this.TABLE_COLUMNS.GROUP_EXPENSES, this.TABLE_COLUMNS.GROUP_TOTAL],
TAG: [this.TABLE_COLUMNS.GROUP_TAG, this.TABLE_COLUMNS.GROUP_EXPENSES, this.TABLE_COLUMNS.GROUP_TOTAL],
MONTH: [this.TABLE_COLUMNS.GROUP_MONTH, this.TABLE_COLUMNS.GROUP_EXPENSES, this.TABLE_COLUMNS.GROUP_TOTAL],
};
},
Expand Down Expand Up @@ -7095,6 +7102,7 @@ const CONST = {
GROUP_WITHDRAWN: 'groupWithdrawn',
GROUP_WITHDRAWAL_ID: 'groupWithdrawalID',
GROUP_CATEGORY: 'groupCategory',
GROUP_TAG: 'groupTag',
GROUP_MONTH: 'groupmonth',
},
SYNTAX_OPERATORS: {
Expand Down Expand Up @@ -7284,6 +7292,7 @@ const CONST = {
[this.TABLE_COLUMNS.GROUP_WITHDRAWN]: 'group-withdrawn',
[this.TABLE_COLUMNS.GROUP_WITHDRAWAL_ID]: 'group-withdrawal-id',
[this.TABLE_COLUMNS.GROUP_CATEGORY]: 'group-category',
[this.TABLE_COLUMNS.GROUP_TAG]: 'group-tag',
};
},
NOT_MODIFIER: 'Not',
Expand Down
15 changes: 15 additions & 0 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
isTransactionListItemType,
isTransactionMemberGroupListItemType,
isTransactionMonthGroupListItemType,
isTransactionTagGroupListItemType,
isTransactionWithdrawalIDGroupListItemType,
shouldShowEmptyState,
shouldShowYear as shouldShowYearUtil,
Expand Down Expand Up @@ -839,6 +840,20 @@ function Search({
return;
}

if (isTransactionTagGroupListItemType(item)) {
const tagValue = item.tag === '' || item.tag === '(untagged)' ? CONST.SEARCH.TAG_EMPTY_VALUE : item.tag;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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.

const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG);
newFlatFilters.push({key: CONST.SEARCH.SYNTAX_FILTER_KEYS.TAG, filters: [{operator: CONST.SEARCH.SYNTAX_OPERATORS.EQUAL_TO, value: tagValue}]});
const newQueryJSON: SearchQueryJSON = {...queryJSON, groupBy: undefined, flatFilters: newFlatFilters};
const newQuery = buildSearchQueryString(newQueryJSON);
const newQueryJSONWithHash = buildSearchQueryJSON(newQuery);
if (!newQueryJSONWithHash) {
return;
}
handleSearch({queryJSON: newQueryJSONWithHash, searchKey, offset: 0, shouldCalculateTotals: false, isLoading: false});
return;
}

if (isTransactionMonthGroupListItemType(item)) {
const newFlatFilters = queryJSON.flatFilters.filter((filter) => filter.key !== CONST.SEARCH.SYNTAX_FILTER_KEYS.DATE);
const {start: monthStart, end: monthEnd} = DateUtils.getMonthDateRange(item.year, item.month);
Expand Down
1 change: 1 addition & 0 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ type SearchCustomColumnIds =
| ValueOf<typeof CONST.SEARCH.GROUP_CUSTOM_COLUMNS.FROM>
| ValueOf<typeof CONST.SEARCH.GROUP_CUSTOM_COLUMNS.WITHDRAWAL_ID>
| ValueOf<typeof CONST.SEARCH.GROUP_CUSTOM_COLUMNS.CATEGORY>
| ValueOf<typeof CONST.SEARCH.GROUP_CUSTOM_COLUMNS.TAG>
| ValueOf<typeof CONST.SEARCH.GROUP_CUSTOM_COLUMNS.MONTH>;

type SearchContextData = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React from 'react';
import {View} from 'react-native';
import Checkbox from '@components/Checkbox';
import type {SearchColumnType} from '@components/Search/types';
import type {ListItem, TransactionTagGroupListItemType} from '@components/SelectionListWithSections/types';
import TextWithTooltip from '@components/TextWithTooltip';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import ExpandCollapseArrowButton from './ExpandCollapseArrowButton';
import TextCell from './TextCell';
import TotalCell from './TotalCell';

type TagListItemHeaderProps<TItem extends ListItem> = {
/** The tag currently being looked at */
tag: TransactionTagGroupListItemType;

/** Callback to fire when a checkbox is pressed */
onCheckboxPress?: (item: TItem) => void;

/** Whether this section items disabled for selection */
isDisabled?: boolean | null;

/** Whether selecting multiple transactions at once is allowed */
canSelectMultiple: boolean | undefined;

/** Whether all transactions are selected */
isSelectAllChecked?: boolean;

/** Whether only some transactions are selected */
isIndeterminate?: boolean;

/** Callback for when the down arrow is clicked */
onDownArrowClick?: () => void;

/** Whether the down arrow is expanded */
isExpanded?: boolean;

/** The visible columns for the header */
columns?: SearchColumnType[];
};

function TagListItemHeader<TItem extends ListItem>({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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.

tag: tagItem,
onCheckboxPress,
isDisabled,
canSelectMultiple,
isSelectAllChecked,
isIndeterminate,
isExpanded,
onDownArrowClick,
columns,
}: TagListItemHeaderProps<TItem>) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {isLargeScreenWidth} = useResponsiveLayout();
const {translate} = useLocalize();

// formattedTag is already translated to "No tag" for empty values in SearchUIUtils
const tagName = tagItem.formattedTag ?? tagItem.tag ?? '';

const columnComponents = {
[CONST.SEARCH.TABLE_COLUMNS.GROUP_TAG]: (
<View
key={CONST.SEARCH.TABLE_COLUMNS.GROUP_TAG}
style={StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.TAG)}
>
<View style={[styles.gap1, styles.flexShrink1]}>
<TextWithTooltip
text={tagName}
style={[styles.optionDisplayName, styles.sidebarLinkTextBold, styles.pre, styles.fontWeightNormal]}
/>
</View>
</View>
),
[CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES]: (
<View
key={CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES}
style={StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.EXPENSES)}
>
<TextCell text={String(tagItem.count)} />
</View>
),
[CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL]: (
<View
key={CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL}
style={StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.TOTAL, false, false, false, false, false, false, false, true)}
>
<TotalCell
total={tagItem.total}
currency={tagItem.currency}
/>
</View>
),
};

return (
<View>
<View style={[styles.pv1Half, styles.pl3, styles.flexRow, styles.alignItemsCenter, isLargeScreenWidth ? styles.gap3 : styles.justifyContentStart]}>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mnh40, styles.flex1, styles.gap3]}>
{!!canSelectMultiple && (
<Checkbox
onPress={() => onCheckboxPress?.(tagItem as unknown as TItem)}
isChecked={isSelectAllChecked}
isIndeterminate={isIndeterminate}
disabled={!!isDisabled || tagItem.isDisabledCheckbox}
accessibilityLabel={translate('common.select')}
style={isLargeScreenWidth && styles.mr1}
/>
)}
{!isLargeScreenWidth && (
<View style={[styles.flexRow, styles.flex1, styles.gap3]}>
<View style={[styles.gap1, styles.flexShrink1]}>
<TextWithTooltip
text={tagName}
style={[styles.optionDisplayName, styles.sidebarLinkTextBold, styles.pre, styles.fontWeightNormal]}
/>
</View>
</View>
)}
{isLargeScreenWidth && columns?.map((column) => columnComponents[column as keyof typeof columnComponents])}
</View>
{!isLargeScreenWidth && (
<View style={[styles.flexShrink0, styles.ml2, styles.mr3, styles.gap1]}>
<TotalCell
total={tagItem.total}
currency={tagItem.currency}
/>
{!!onDownArrowClick && (
<ExpandCollapseArrowButton
isExpanded={isExpanded}
onPress={onDownArrowClick}
/>
)}
</View>
)}
</View>
</View>
);
}

export default TagListItemHeader;
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
TransactionMemberGroupListItemType,
TransactionMonthGroupListItemType,
TransactionReportGroupListItemType,
TransactionTagGroupListItemType,
TransactionWithdrawalIDGroupListItemType,
} from '@components/SelectionListWithSections/types';
import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
Expand All @@ -45,6 +46,7 @@ import CategoryListItemHeader from './CategoryListItemHeader';
import MemberListItemHeader from './MemberListItemHeader';
import MonthListItemHeader from './MonthListItemHeader';
import ReportListItemHeader from './ReportListItemHeader';
import TagListItemHeader from './TagListItemHeader';
import TransactionGroupListExpandedItem from './TransactionGroupListExpanded';
import WithdrawalIDListItemHeader from './WithdrawalIDListItemHeader';

Expand Down Expand Up @@ -298,6 +300,19 @@ function TransactionGroupListItem<TItem extends ListItem>({
isExpanded={isExpanded}
/>
),
[CONST.SEARCH.GROUP_BY.TAG]: (
<TagListItemHeader
tag={groupItem as TransactionTagGroupListItemType}
onCheckboxPress={onCheckboxPress}
isDisabled={isDisabledOrEmpty}
columns={columns}
canSelectMultiple={canSelectMultiple}
isSelectAllChecked={isSelectAllChecked}
isIndeterminate={isIndeterminate}
onDownArrowClick={onExpandIconPress}
isExpanded={isExpanded}
/>
),
[CONST.SEARCH.GROUP_BY.MONTH]: (
<MonthListItemHeader
month={groupItem as TransactionMonthGroupListItemType}
Expand Down
18 changes: 18 additions & 0 deletions src/components/SelectionListWithSections/SearchTableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,24 @@ const getTransactionGroupHeaders = (groupBy: SearchGroupBy, icons: SearchHeaderI
isColumnSortable: true,
},
];
case CONST.SEARCH.GROUP_BY.TAG:
return [
{
columnName: CONST.SEARCH.TABLE_COLUMNS.GROUP_TAG,
translationKey: 'common.tag',
isColumnSortable: true,
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.GROUP_EXPENSES,
translationKey: 'common.expenses',
isColumnSortable: true,
},
{
columnName: CONST.SEARCH.TABLE_COLUMNS.GROUP_TOTAL,
translationKey: 'common.total',
isColumnSortable: true,
},
];
case CONST.SEARCH.GROUP_BY.MONTH:
return [
{
Expand Down
7 changes: 7 additions & 0 deletions src/components/SelectionListWithSections/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
SearchDataTypes,
SearchMemberGroup,
SearchMonthGroup,
SearchTagGroup,
SearchTask,
SearchTransactionAction,
SearchWithdrawalIDGroup,
Expand Down Expand Up @@ -511,6 +512,11 @@ type TransactionCategoryGroupListItemType = TransactionGroupListItemType & {grou
formattedCategory?: string;
};

type TransactionTagGroupListItemType = TransactionGroupListItemType & {groupedBy: typeof CONST.SEARCH.GROUP_BY.TAG} & SearchTagGroup & {
/** Final and formatted "tag" value used for displaying and sorting */
formattedTag?: string;
};

type ListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {
/** The section list item */
item: TItem;
Expand Down Expand Up @@ -1154,6 +1160,7 @@ export type {
TransactionCardGroupListItemType,
TransactionWithdrawalIDGroupListItemType,
TransactionCategoryGroupListItemType,
TransactionTagGroupListItemType,
Section,
SectionListDataType,
SectionWithIndexOffset,
Expand Down
3 changes: 2 additions & 1 deletion src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ const translations: TranslationDeepObject<typeof en> = {
expiredCodeDescription: 'Gehe zurück zum ursprünglichen Gerät und fordere einen neuen Code an',
successfulNewCodeRequest: 'Code angefordert. Bitte überprüfe dein Gerät.',
tfaRequiredTitle: dedent(`
Zwei-Faktor-Authentifizierung
Zwei-Faktor-Authentifizierung
erforderlich
`),
tfaRequiredDescription: dedent(`
Expand Down Expand Up @@ -6955,6 +6955,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]: 'Stichwort',
[CONST.SEARCH.GROUP_BY.MONTH]: 'Monat',
},
feed: 'Feed',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6839,6 +6839,7 @@ const translations = {
[CONST.SEARCH.GROUP_BY.CARD]: 'Card',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'Withdrawal ID',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Category',
[CONST.SEARCH.GROUP_BY.TAG]: 'Tag',
[CONST.SEARCH.GROUP_BY.MONTH]: 'Month',
},
feed: 'Feed',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6589,6 +6589,7 @@ ${amount} para ${merchant} - ${date}`,
[CONST.SEARCH.GROUP_BY.CARD]: 'Tarjeta',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID de retiro',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categoría',
[CONST.SEARCH.GROUP_BY.TAG]: 'Etiqueta',
[CONST.SEARCH.GROUP_BY.MONTH]: 'Mes',
},
feed: 'Feed',
Expand Down
5 changes: 3 additions & 2 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ const translations: TranslationDeepObject<typeof en> = {
expiredCodeDescription: 'Retournez sur l’appareil d’origine et demandez un nouveau code',
successfulNewCodeRequest: 'Code demandé. Veuillez vérifier votre appareil.',
tfaRequiredTitle: dedent(`
Authentification à deux facteurs
Authentification à deux facteurs
requise
`),
tfaRequiredDescription: dedent(`
Expand Down Expand Up @@ -4672,7 +4672,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.

_Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.NETSUITE_IMPORT.HELP_LINKS.CUSTOM_SEGMENTS})_.`,
customSegmentScriptIDTitle: 'Quel est l’ID du script ?',
customSegmentScriptIDFooter: `Vous pouvez trouver les ID de script de segment personnalisé dans NetSuite sous :
customSegmentScriptIDFooter: `Vous pouvez trouver les ID de script de segment personnalisé dans NetSuite sous :

1. *Customization > Lists, Records, & Fields > Custom Segments*.
2. Cliquez sur un segment personnalisé.
Expand Down Expand Up @@ -6966,6 +6966,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
[CONST.SEARCH.GROUP_BY.CARD]: 'Carte',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID de retrait',
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Catégorie',
[CONST.SEARCH.GROUP_BY.TAG]: 'Étiquette',
[CONST.SEARCH.GROUP_BY.MONTH]: 'Mois',
},
feed: 'Flux',
Expand Down
3 changes: 2 additions & 1 deletion src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ const translations: TranslationDeepObject<typeof en> = {
expiredCodeDescription: 'Torna al dispositivo originale e richiedi un nuovo codice',
successfulNewCodeRequest: 'Codice richiesto. Controlla il tuo dispositivo.',
tfaRequiredTitle: dedent(`
Autenticazione a due fattori
Autenticazione a due fattori
richiesta
`),
tfaRequiredDescription: dedent(`
Expand Down Expand Up @@ -6944,6 +6944,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
[CONST.SEARCH.GROUP_BY.CARD]: 'Carta',
[CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID prelievo', //_/\__/_/ \_,_/\__/\__/\_,_/
[CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categoria',
[CONST.SEARCH.GROUP_BY.TAG]: 'Etichetta',
[CONST.SEARCH.GROUP_BY.MONTH]: 'Mese',
},
feed: 'Feed',
Expand Down
Loading
Loading