Skip to content

Commit cb1fbb1

Browse files
authored
Merge pull request Expensify#76403 from TaduJR/feat-CFI-Add-group-by-report-layout-feature
chore: [CFI] Add group-by report layout feature
2 parents 3977046 + db8588d commit cb1fbb1

File tree

6 files changed

+482
-24
lines changed

6 files changed

+482
-24
lines changed

src/components/MoneyRequestReportView/MoneyRequestReportGroupHeader.tsx

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, {useCallback, useMemo} from 'react';
22
import {View} from 'react-native';
33
import Checkbox from '@components/Checkbox';
4+
import OfflineWithFeedback from '@components/OfflineWithFeedback';
45
import Text from '@components/Text';
56
import useLocalize from '@hooks/useLocalize';
67
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -10,6 +11,7 @@ import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils';
1011
import variables from '@styles/variables';
1112
import CONST from '@src/CONST';
1213
import type {GroupedTransactions} from '@src/types/onyx';
14+
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
1315

1416
// Height constants
1517
const DESKTOP_HEIGHT = 28;
@@ -43,6 +45,9 @@ type MoneyRequestReportGroupHeaderProps = {
4345

4446
/** Callback when group checkbox is toggled - receives groupKey */
4547
onToggleSelection?: (groupKey: string) => void;
48+
49+
/** Pending action for offline feedback styling (Pattern B - Optimistic WITH Feedback) */
50+
pendingAction?: PendingAction;
4651
};
4752

4853
function MoneyRequestReportGroupHeader({
@@ -55,6 +60,7 @@ function MoneyRequestReportGroupHeader({
5560
isIndeterminate = false,
5661
isDisabled = false,
5762
onToggleSelection,
63+
pendingAction,
5864
}: MoneyRequestReportGroupHeaderProps) {
5965
const styles = useThemeStyles();
6066
const {translate} = useLocalize();
@@ -84,28 +90,30 @@ function MoneyRequestReportGroupHeader({
8490
}, [onToggleSelection, groupKey]);
8591

8692
return (
87-
<View style={[styles.reportLayoutGroupHeader, conditionalHeight]}>
88-
<View style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]}>
89-
{shouldShowCheckbox && (
90-
<Checkbox
91-
isChecked={isSelected}
92-
isIndeterminate={isIndeterminate}
93-
disabled={isDisabled}
94-
onPress={handleToggleSelection}
95-
accessibilityLabel={translate('reportLayout.selectGroup', {groupName: displayName})}
96-
style={styles.mr2}
97-
/>
98-
)}
99-
<Text
100-
style={[styles.textBold, textStyle, styles.flexShrink1, shouldShowCheckbox && styles.ml2]}
101-
numberOfLines={1}
102-
>
103-
{displayName}
104-
</Text>
105-
<Text style={[styles.textBold, textStyle, styles.mh1]}>{CONST.DOT_SEPARATOR}</Text>
106-
<Text style={[styles.textBold, textStyle]}>{formattedAmount}</Text>
93+
<OfflineWithFeedback pendingAction={pendingAction}>
94+
<View style={[styles.reportLayoutGroupHeader, conditionalHeight]}>
95+
<View style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]}>
96+
{shouldShowCheckbox && (
97+
<Checkbox
98+
isChecked={isSelected}
99+
isIndeterminate={isIndeterminate}
100+
disabled={isDisabled}
101+
onPress={handleToggleSelection}
102+
accessibilityLabel={translate('reportLayout.selectGroup', {groupName: displayName})}
103+
style={styles.mr2}
104+
/>
105+
)}
106+
<Text
107+
style={[styles.textBold, textStyle, styles.flexShrink1, shouldShowCheckbox && styles.ml2]}
108+
numberOfLines={1}
109+
>
110+
{displayName}
111+
</Text>
112+
<Text style={[styles.textBold, textStyle, styles.mh1]}>{CONST.DOT_SEPARATOR}</Text>
113+
<Text style={[styles.textBold, textStyle]}>{formattedAmount}</Text>
114+
</View>
107115
</View>
108-
</View>
116+
</OfflineWithFeedback>
109117
);
110118
}
111119

src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
6767
import ROUTES from '@src/ROUTES';
6868
import type SCREENS from '@src/SCREENS';
6969
import type * as OnyxTypes from '@src/types/onyx';
70+
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
7071
import MoneyRequestReportGroupHeader from './MoneyRequestReportGroupHeader';
7172
import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader';
7273
import MoneyRequestReportTotalSpend from './MoneyRequestReportTotalSpend';
@@ -298,13 +299,14 @@ function MoneyRequestReportTransactionList({
298299
}, [sortedTransactions]);
299300

300301
const groupSelectionState = useMemo(() => {
301-
const state = new Map<string, {isSelected: boolean; isIndeterminate: boolean; isDisabled: boolean}>();
302+
const state = new Map<string, {isSelected: boolean; isIndeterminate: boolean; isDisabled: boolean; pendingAction?: PendingAction}>();
302303

303304
for (const group of groupedTransactions) {
304305
const groupTransactionIDs = group.transactions.filter((t) => !isTransactionPendingDelete(t)).map((t) => t.transactionID);
306+
const groupPendingAction = group.transactions.some((t) => getTransactionPendingAction(t)) ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : undefined;
305307

306308
if (groupTransactionIDs.length === 0) {
307-
state.set(group.groupKey, {isSelected: false, isIndeterminate: false, isDisabled: true});
309+
state.set(group.groupKey, {isSelected: false, isIndeterminate: false, isDisabled: true, pendingAction: groupPendingAction});
308310
continue;
309311
}
310312

@@ -313,6 +315,7 @@ function MoneyRequestReportTransactionList({
313315
isSelected: selectedCount === groupTransactionIDs.length,
314316
isIndeterminate: selectedCount > 0 && selectedCount < groupTransactionIDs.length,
315317
isDisabled: false,
318+
pendingAction: groupPendingAction,
316319
});
317320
}
318321

@@ -487,7 +490,7 @@ function MoneyRequestReportTransactionList({
487490
<View style={[listHorizontalPadding, styles.gap2, styles.pb4]}>
488491
{shouldShowGroupedTransactions
489492
? groupedTransactions.map((group) => {
490-
const selectionState = groupSelectionState.get(group.groupKey) ?? {isSelected: false, isIndeterminate: false, isDisabled: false};
493+
const selectionState = groupSelectionState.get(group.groupKey) ?? {isSelected: false, isIndeterminate: false, isDisabled: false, pendingAction: undefined};
491494

492495
return (
493496
<View
@@ -504,6 +507,7 @@ function MoneyRequestReportTransactionList({
504507
isIndeterminate={selectionState.isIndeterminate}
505508
isDisabled={selectionState.isDisabled}
506509
onToggleSelection={toggleGroupSelection}
510+
pendingAction={selectionState.pendingAction}
507511
/>
508512
{group.transactions.map((transaction) => {
509513
const originalTransaction = sortedTransactionsMap.get(transaction.transactionID) ?? transaction;

tests/actions/ReportLayoutTest.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {getReportLayoutGroupBy} from '@libs/actions/ReportLayout';
2+
import CONST from '@src/CONST';
3+
4+
describe('getReportLayoutGroupBy', () => {
5+
it('returns CATEGORY when storedValue is null', () => {
6+
expect(getReportLayoutGroupBy(null)).toBe(CONST.REPORT_LAYOUT.GROUP_BY.CATEGORY);
7+
});
8+
9+
it('returns CATEGORY when storedValue is undefined', () => {
10+
expect(getReportLayoutGroupBy(undefined)).toBe(CONST.REPORT_LAYOUT.GROUP_BY.CATEGORY);
11+
});
12+
13+
it('returns CATEGORY when storedValue is empty string', () => {
14+
expect(getReportLayoutGroupBy('')).toBe(CONST.REPORT_LAYOUT.GROUP_BY.CATEGORY);
15+
});
16+
17+
it('returns the stored value when it is CATEGORY', () => {
18+
expect(getReportLayoutGroupBy(CONST.REPORT_LAYOUT.GROUP_BY.CATEGORY)).toBe(CONST.REPORT_LAYOUT.GROUP_BY.CATEGORY);
19+
});
20+
21+
it('returns the stored value when it is TAG', () => {
22+
expect(getReportLayoutGroupBy(CONST.REPORT_LAYOUT.GROUP_BY.TAG)).toBe(CONST.REPORT_LAYOUT.GROUP_BY.TAG);
23+
});
24+
25+
it('returns the stored value as-is for any non-empty string', () => {
26+
expect(getReportLayoutGroupBy('customValue')).toBe('customValue');
27+
});
28+
});

0 commit comments

Comments
 (0)