Skip to content

Commit c0b3895

Browse files
Merge pull request #85292 from TaduJR/fix-Reports-Total-report-number-x-of-y-does-not-update-when-new-report-is-created-offline
fix: filter deleted reports from navigation and disable in search list
2 parents a8d4105 + d6718b0 commit c0b3895

6 files changed

Lines changed: 269 additions & 8 deletions

File tree

src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
2222
import {handleActionButtonPress} from '@libs/actions/Search';
2323
import {syncMissingAttendeesViolation} from '@libs/AttendeeUtils';
2424
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
25-
import {isInvoiceReport, isOpenExpenseReport, isProcessingReport} from '@libs/ReportUtils';
25+
import {isInvoiceReport, isOpenExpenseReport, isProcessingReport, isReportPendingDelete} from '@libs/ReportUtils';
2626
import {isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils';
2727
import variables from '@styles/variables';
2828
import CONST from '@src/CONST';
@@ -82,6 +82,10 @@ function ExpenseReportListItem<TItem extends ListItem>({
8282
return reportItem.isDisabled ?? reportItem.isDisabledCheckbox;
8383
}, [reportItem.isDisabled, reportItem.isDisabledCheckbox]);
8484

85+
// Prefer live Onyx data over stale search snapshot for pending delete check.
86+
const reportForPendingDeleteCheck = parentReport ?? reportItem;
87+
const isPendingDelete = isReportPendingDelete(reportForPendingDeleteCheck);
88+
8589
// Prefer live Onyx policy data over snapshot to ensure fresh policy settings
8690
// like isAttendeeTrackingEnabled is not missing
8791
// Use snapshotReport/snapshotPolicy as fallbacks to fix offline issues where
@@ -162,8 +166,9 @@ function ExpenseReportListItem<TItem extends ListItem>({
162166
styles.bgTransparent,
163167
item.isSelected && styles.activeComponentBG,
164168
styles.mh0,
169+
isPendingDelete && styles.cursorDisabled,
165170
],
166-
[styles, item.isSelected],
171+
[styles, item.isSelected, isPendingDelete],
167172
);
168173

169174
const listItemWrapperStyle = useMemo(
@@ -237,10 +242,12 @@ function ExpenseReportListItem<TItem extends ListItem>({
237242
onLongPressRow={onLongPressRow}
238243
shouldSyncFocus={shouldSyncFocus}
239244
hoverStyle={item.isSelected && styles.activeComponentBG}
240-
pressableWrapperStyle={[styles.mh5, animatedHighlightStyle]}
245+
pressableWrapperStyle={[styles.mh5, animatedHighlightStyle, isPendingDelete && styles.cursorDisabled]}
241246
accessible={false}
242247
shouldShowRightCaret={false}
243248
shouldUseDefaultRightHandSideCheckmark={false}
249+
isDisabled={isPendingDelete}
250+
shouldDisableHoverStyle={isPendingDelete}
244251
>
245252
{(hovered) => (
246253
<View style={[styles.flex1]}>
@@ -258,6 +265,7 @@ function ExpenseReportListItem<TItem extends ListItem>({
258265
isDisabledCheckbox={isDisabledCheckbox}
259266
isHovered={hovered}
260267
isFocused={isFocused}
268+
isPendingDelete={isPendingDelete}
261269
/>
262270
{getDescription}
263271
</View>

src/components/Search/SearchList/ListItem/ExpenseReportListItemRow.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type ExpenseReportListItemRowProps = {
4747
isDisabledCheckbox?: boolean;
4848
isHovered?: boolean;
4949
isFocused?: boolean;
50+
isPendingDelete?: boolean;
5051
columns?: SearchColumnType[];
5152
};
5253

@@ -65,6 +66,7 @@ function ExpenseReportListItemRow({
6566
columns = [],
6667
isHovered = false,
6768
isFocused = false,
69+
isPendingDelete = false,
6870
}: ExpenseReportListItemRowProps) {
6971
const StyleUtils = useStyleUtils();
7072
const styles = useThemeStyles();
@@ -207,6 +209,7 @@ function ExpenseReportListItemRow({
207209
reportID={item.reportID}
208210
hash={item.hash}
209211
amount={item.total}
212+
shouldDisablePointerEvents={isPendingDelete}
210213
/>
211214
</View>
212215
),
@@ -312,7 +315,7 @@ function ExpenseReportListItemRow({
312315
hash={item.hash}
313316
amount={item.total}
314317
extraSmall
315-
shouldDisablePointerEvents={isInMobileSelectionMode}
318+
shouldDisablePointerEvents={isInMobileSelectionMode || isPendingDelete}
316319
/>
317320
</View>
318321
</View>

src/hooks/useSearchSections.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
1-
import type {OnyxEntry} from 'react-native-onyx';
2-
import {selectFilteredReportActions} from '@libs/ReportUtils';
1+
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
2+
import {isReportPendingDelete, selectFilteredReportActions} from '@libs/ReportUtils';
33
import {getSections, getSortedSections} from '@libs/SearchUIUtils';
4+
import CONST from '@src/CONST';
45
import ONYXKEYS from '@src/ONYXKEYS';
6+
import type {Report} from '@src/types/onyx';
57
import type LastSearchParams from '@src/types/onyx/ReportNavigation';
68
import useActionLoadingReportIDs from './useActionLoadingReportIDs';
79
import useArchivedReportsIdSet from './useArchivedReportsIdSet';
810
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
911
import useLocalize from './useLocalize';
1012
import useOnyx from './useOnyx';
1113

14+
/**
15+
* Returns sorted keys of reports pending deletion.
16+
* Sorted string[] keeps Onyx comparison cheap (PERF-11).
17+
*/
18+
const selectPendingDeleteReportKeys = (reports: OnyxCollection<Report>): string[] => {
19+
const keys: string[] = [];
20+
for (const [key, report] of Object.entries(reports ?? {})) {
21+
if (isReportPendingDelete(report)) {
22+
keys.push(key);
23+
}
24+
}
25+
return keys.sort();
26+
};
27+
1228
type UseSearchSectionsResult = {
1329
allReports: Array<string | undefined>;
1430
isSearchLoading: boolean;
@@ -18,6 +34,8 @@ type UseSearchSectionsResult = {
1834
function useSearchSections(): UseSearchSectionsResult {
1935
const [lastSearchQuery] = useOnyx(ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY);
2036
const [currentSearchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${lastSearchQuery?.queryJSON?.hash}`);
37+
const [pendingDeleteReportKeys = CONST.EMPTY_ARRAY] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: selectPendingDeleteReportKeys});
38+
const pendingDeleteReportKeysSet = new Set(pendingDeleteReportKeys);
2139
const currentUserDetails = useCurrentUserPersonalDetails();
2240
const {localeCompare, formatPhoneNumber, translate} = useLocalize();
2341
const isActionLoadingSet = useActionLoadingReportIDs();
@@ -42,7 +60,7 @@ function useSearchSections(): UseSearchSectionsResult {
4260
const currentUserEmail = currentUserDetails.email ?? '';
4361
const searchKey = lastSearchQuery?.searchKey;
4462

45-
let allReports: Array<string | undefined> = [];
63+
let results: Array<string | undefined> = [];
4664
if (!!type && !!searchResultsData && !!searchResultsSearch) {
4765
const [searchData] = getSections({
4866
type,
@@ -62,10 +80,18 @@ function useSearchSections(): UseSearchSectionsResult {
6280
cardList,
6381
conciergeReportID,
6482
});
65-
allReports = getSortedSections(type, status ?? '', searchData, localeCompare, translate, sortBy, sortOrder, groupBy).map((value) => value.reportID);
83+
results = getSortedSections(type, status ?? '', searchData, localeCompare, translate, sortBy, sortOrder, groupBy).map((value) => value.reportID);
6684
}
6785

86+
const allReports = results.filter((id) => {
87+
if (!id) {
88+
return false;
89+
}
90+
return !pendingDeleteReportKeysSet.has(`${ONYXKEYS.COLLECTION.REPORT}${id}`);
91+
});
92+
6893
return {allReports, isSearchLoading: !!currentSearchResults?.search?.isLoading, lastSearchQuery};
6994
}
7095

96+
export {selectPendingDeleteReportKeys};
7197
export default useSearchSections;

src/libs/ReportUtils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10252,6 +10252,14 @@ function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry<Report> | str
1025210252
return parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
1025310253
}
1025410254

10255+
/**
10256+
* Check if a report itself is pending deletion via its own pendingAction or pendingFields.preview.
10257+
* Unlike isMoneyRequestReportPendingDeletion (which checks the parent report action), this checks the report directly.
10258+
*/
10259+
function isReportPendingDelete(report: OnyxEntry<Pick<Report, 'pendingAction' | 'pendingFields'>>): boolean {
10260+
return report?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || report?.pendingFields?.preview === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
10261+
}
10262+
1025510263
function navigateToLinkedReportAction(
1025610264
ancestor: Ancestor,
1025710265
isInNarrowPaneModal: boolean,
@@ -13473,6 +13481,7 @@ export {
1347313481
isMoneyRequest,
1347413482
isMoneyRequestReport,
1347513483
isMoneyRequestReportPendingDeletion,
13484+
isReportPendingDelete,
1347613485
isOneOnOneChat,
1347713486
isOneTransactionThread,
1347813487
isOpenExpenseReport,

tests/unit/ReportUtilsTest.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ import {
119119
isPayer,
120120
isReportIneligibleForMoveExpenses,
121121
isReportOutstanding,
122+
isReportPendingDelete,
122123
isRootGroupChat,
123124
isSelfDMOrSelfDMThread,
124125
isWorkspaceMemberLeavingWorkspaceRoom,
@@ -5006,6 +5007,32 @@ describe('ReportUtils', () => {
50065007
});
50075008
});
50085009

5010+
describe('isReportPendingDelete', () => {
5011+
it('should return true when pendingAction is DELETE', () => {
5012+
expect(isReportPendingDelete({pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE})).toBe(true);
5013+
});
5014+
5015+
it('should return true when pendingFields.preview is DELETE', () => {
5016+
expect(isReportPendingDelete({pendingFields: {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}})).toBe(true);
5017+
});
5018+
5019+
it('should return true when both pendingAction and pendingFields.preview are DELETE', () => {
5020+
expect(isReportPendingDelete({pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, pendingFields: {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}})).toBe(true);
5021+
});
5022+
5023+
it('should return false when neither field indicates deletion', () => {
5024+
expect(isReportPendingDelete({pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD})).toBe(false);
5025+
});
5026+
5027+
it('should return false for undefined report', () => {
5028+
expect(isReportPendingDelete(undefined)).toBe(false);
5029+
});
5030+
5031+
it('should return false when pendingFields exists but preview is not DELETE', () => {
5032+
expect(isReportPendingDelete({pendingFields: {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}})).toBe(false);
5033+
});
5034+
});
5035+
50095036
describe('canEditMoneyRequest', () => {
50105037
it('it should return false for archived invoice', async () => {
50115038
const invoiceReport: Report = {

0 commit comments

Comments
 (0)