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
1 change: 1 addition & 0 deletions config/eslint/eslint.seatbelt.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@
"../../src/hooks/useReviewDuplicatesNavigation.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useReviewDuplicatesNavigation.tsx" "react-hooks/set-state-in-effect" 1
"../../src/hooks/useSearchBulkActions.ts" "@typescript-eslint/no-unsafe-type-assertion" 10
"../../src/hooks/useSearchHighlightAndScroll.ts" "@typescript-eslint/no-unsafe-type-assertion" 4
"../../src/hooks/useSidebarOrderedReports.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/hooks/useSidebarOrderedReports.tsx" "react-hooks/refs" 5
"../../src/hooks/useSidebarOrderedReports.tsx" "react-hooks/set-state-in-effect" 1
Expand Down
4 changes: 0 additions & 4 deletions src/GlobalModals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import React, {startTransition, useEffect, useState} from 'react';

import DelegateNoAccessModalProvider from './components/DelegateNoAccessModalProvider';
import EmojiPicker from './components/EmojiPicker/EmojiPicker';
import ExpenseAddedGrowl from './components/ExpenseAddedGrowl';
import GrowlNotification from './components/GrowlNotification';
import LazyModalSlot from './components/LazyModalSlot';
import useIsAuthenticated from './hooks/useIsAuthenticated';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
import {growlRef} from './libs/Growl';
import * as ReportActionContextMenu from './pages/inbox/report/ContextMenu/ReportActionContextMenu';
Expand All @@ -26,7 +24,6 @@ const IDLE_CALLBACK_TIMEOUT_MS = 2000;
function GlobalModals() {
const [shouldRenderContextMenu, setShouldRenderContextMenu] = useState(false);
const [shouldRenderDeferredModals, setShouldRenderDeferredModals] = useState(false);
const isAuthenticated = useIsAuthenticated();

// Defer loading the context menu and rare-condition modals until after startup to avoid
// pulling in their dependencies (ContextMenuActions, ReportUtils, ModifiedExpenseMessage,
Expand All @@ -53,7 +50,6 @@ function GlobalModals() {
return (
<>
<GrowlNotification ref={growlRef} />
{isAuthenticated && <ExpenseAddedGrowl />}
<DelegateNoAccessModalProvider>
{shouldRenderContextMenu && (
<LazyModalSlot>
Expand Down
7 changes: 3 additions & 4 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type {Attendee, DistanceExpenseType, Participant} from './types/onyx/IOU'
import type Onboarding from './types/onyx/Onboarding';
import type {AnyOnyxUpdate} from './types/onyx/Request';
import type {SavedCSVColumnLayoutList} from './types/onyx/SavedCSVColumnLayout';
import type {SearchDataTypes} from './types/onyx/SearchResults';
import type AssertTypesEqual from './types/utils/AssertTypesEqual';
import type DeepValueOf from './types/utils/DeepValueOf';

Expand Down Expand Up @@ -852,8 +851,8 @@ const ONYXKEYS = {
/** Whether the user has denied the contact import permission prompt */
HAS_DENIED_CONTACT_IMPORT_PROMPT: 'hasDeniedContactImportPrompt',

/** Maps each newly-added transaction ID to its search data type, flagging it for the "Expense added" growl */
EXPENSE_ADDED_GROWL_TRANSACTION_IDS: 'expenseAddedGrowlTransactionIDs',
/** The transaction IDs to be highlighted when opening the Expenses search route page */
TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE: 'transactionIdsHighlightOnSearchRoute',

/** The report ID to be highlighted when returning to the workspace rooms page */
ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE: 'roomIDHighlightOnRoomsPage',
Expand Down Expand Up @@ -1823,7 +1822,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_REPORT_DETAILS_COLUMNS]: string[];
[ONYXKEYS.HAS_DENIED_CONTACT_IMPORT_PROMPT]: boolean | undefined;
[ONYXKEYS.PERSONAL_POLICY_ID]: string;
[ONYXKEYS.EXPENSE_ADDED_GROWL_TRANSACTION_IDS]: Record<string, SearchDataTypes>;
[ONYXKEYS.TRANSACTION_IDS_HIGHLIGHT_ON_SEARCH_ROUTE]: Record<string, Record<string, boolean>>;
[ONYXKEYS.ROOM_ID_HIGHLIGHT_ON_ROOMS_PAGE]: string | null;
[ONYXKEYS.DOMAIN_GROUP_CREATE_PREFERRED_POLICY_ID]: string | undefined;
};
Expand Down
164 changes: 0 additions & 164 deletions src/components/ExpenseAddedGrowl.tsx

This file was deleted.

6 changes: 5 additions & 1 deletion src/components/Search/ChatSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CONST from '@src/CONST';

import type {NativeSyntheticEvent} from 'react-native';

import React from 'react';
import React, {useImperativeHandle} from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps} from './searchViewProps';
Expand Down Expand Up @@ -49,6 +49,7 @@ function ChatSearchView({
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: ChatSearchViewProps) {
const {type} = queryJSON;

Expand All @@ -64,6 +65,9 @@ function ChatSearchView({
const selectedItemsLength = data.reduce((acc, item) => acc + (item.keyForList && selectedTransactions[item.keyForList]?.isSelected ? 1 : 0), 0);
const totalItems = data.filter((item) => !isRowDeleted(item)).length;

// Flat data maps 1:1 to the rendered list, so highlight-scroll-to-index is the same as scroll-to-data-index.
useImperativeHandle(ref, () => ({scrollToIndex: scrollToListIndex}), [scrollToListIndex]);

const renderItem = (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent<ExtendedTargetedEvent>) => void) => (
// Chat rows never animate their exit (only grouped expenses do), so the wrapper just preserves the overflow clip.
<AnimatedExitRow
Expand Down
15 changes: 10 additions & 5 deletions src/components/Search/ExpenseFlatSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CONST from '@src/CONST';

import type {NativeSyntheticEvent} from 'react-native';

import React from 'react';
import React, {useImperativeHandle} from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps, TransactionViewExtras} from './searchViewProps';
Expand All @@ -29,10 +29,11 @@ const isRowDeleted = (item: SearchListItem) => item.pendingAction === CONST.RED_
* `toggle`/`toggleAll`/`selectedTransactions`). The shared list state and interactions come from
* `useSearchListViewState`, and the surrounding chrome (horizontal scroll, header bar, long-press menu)
* from `SearchListViewLayout`; this view only owns the flat-expense specifics: the `TransactionListItem`
* renderer and single-pass visibility/selection counts.
* `TransactionListItem` is the only row renderer here, so the group/sticky/chat/task branches of
* `SearchList` do not apply. Keyboard navigation is inherited from `BaseSearchList`; `newTransactions`
* flows into `extraData` so the list re-renders when a freshly-created expense lands.
* renderer, single-pass visibility/selection counts, and the highlight-scroll imperative handle.
* `TransactionListItem` is the only row renderer here and rows always animate, so the
* group/sticky/chat/task branches of `SearchList` do not apply. Keyboard navigation is inherited from
* `BaseSearchList`; the post-create highlight stays in the router (the snapshot stamps
* `shouldAnimateInHighlight`, and `newTransactions` flows into `extraData`).
*/
function ExpenseFlatSearchView({
queryJSON,
Expand All @@ -54,6 +55,7 @@ function ExpenseFlatSearchView({
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: ExpenseFlatSearchViewProps) {
const {type} = queryJSON;

Expand Down Expand Up @@ -87,6 +89,9 @@ function ExpenseFlatSearchView({
const selectedItemsLength = data.reduce((acc, item) => acc + (item.keyForList && selectedTransactions[item.keyForList]?.isSelected ? 1 : 0), 0);
const totalItems = data.filter((item) => !isRowDeleted(item)).length;

// Flat data maps 1:1 to the rendered list, so highlight-scroll-to-index is the same as scroll-to-data-index.
useImperativeHandle(ref, () => ({scrollToIndex: scrollToListIndex}), [scrollToListIndex]);

const renderItem = (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent<ExtendedTargetedEvent>) => void) => {
const isDisabled = isRowDeleted(item);
// Only expense row exits animate; invoice and trip transaction lists do not (matches the legacy per-type gate).
Expand Down
26 changes: 24 additions & 2 deletions src/components/Search/ExpenseGroupedSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {Transaction} from '@src/types/onyx';

import type {NativeSyntheticEvent} from 'react-native';

import React, {useState} from 'react';
import React, {useImperativeHandle, useState} from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps, TransactionViewExtras} from './searchViewProps';
Expand Down Expand Up @@ -89,7 +89,8 @@ function buildNewTransactionIDMap(data: SearchListItem[], newTransactions: Trans
* `useSearchListViewState`, and the surrounding chrome from `SearchListViewLayout`. This view owns the group
* machinery: on wide web it splits each group into a sticky `GroupHeader` plus an expandable
* `GroupChildrenContainer` (`shouldSplitGroups`); otherwise each group renders through `TransactionGroupListItem`.
* Selection counts are report-aware (flattened over child transactions plus empty groups).
* Selection counts are report-aware (flattened over child transactions plus empty groups), and the scroll
* handle remaps the router's data index to the split-list index.
*/
function ExpenseGroupedSearchView({
queryJSON,
Expand All @@ -111,6 +112,7 @@ function ExpenseGroupedSearchView({
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: ExpenseGroupedSearchViewProps) {
const {type, groupBy} = queryJSON;
const {isLargeScreenWidth} = useResponsiveLayout();
Expand Down Expand Up @@ -170,6 +172,26 @@ function ExpenseGroupedSearchView({
const firstVisibleIndex = listData.findIndex(isItemVisible);
const lastVisibleIndex = listData.findLastIndex(isItemVisible);

// The router highlights by source-data index; remap it to the split-list index before scrolling.
useImperativeHandle(
ref,
() => ({
scrollToIndex: (index: number, animated = true) => {
if (!shouldSplit) {
scrollToListIndex(index, animated);
return;
}
const item = data.at(index);
if (!item) {
return;
}
const splitIndex = item.keyForList ? listData.findIndex((listItem) => listItem.keyForList === `header_${item.keyForList}`) : -1;
scrollToListIndex(splitIndex !== -1 ? splitIndex : index, animated);
},
}),
[data, listData, shouldSplit, scrollToListIndex],
);

const getItemType = (item: SearchListItem) => {
if (!shouldSplit) {
return undefined;
Expand Down
6 changes: 5 additions & 1 deletion src/components/Search/ExpenseReportSearchView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import CONST from '@src/CONST';

import type {NativeSyntheticEvent} from 'react-native';

import React from 'react';
import React, {useImperativeHandle} from 'react';

import type {SearchListItem} from './SearchList/ListItem/types';
import type {CommonSearchViewProps} from './searchViewProps';
Expand Down Expand Up @@ -55,6 +55,7 @@ function ExpenseReportSearchView({
onScroll,
contentContainerStyle,
containerStyle,
ref,
}: ExpenseReportSearchViewProps) {
const {type} = queryJSON;

Expand Down Expand Up @@ -91,6 +92,9 @@ function ExpenseReportSearchView({
emptyReports.reduce((acc, item) => acc + (isRowSelected(item.keyForList, selectedTransactions) ? 1 : 0), 0);
const totalItems = flattenedTransactions.filter((item) => !isRowDeleted(item)).length + emptyReports.filter((item) => !isRowDeleted(item)).length;

// Report data maps 1:1 to the rendered list, so highlight-scroll-to-index is the same as scroll-to-data-index.
useImperativeHandle(ref, () => ({scrollToIndex: scrollToListIndex}), [scrollToListIndex]);

const renderItem = (item: SearchListItem, index: number, isItemFocused: boolean, onFocus?: (event: NativeSyntheticEvent<ExtendedTargetedEvent>) => void) => (
// Report rows never animate their exit (only grouped expenses do), so the wrapper just preserves the overflow clip.
<AnimatedExitRow
Expand Down
Loading
Loading