Skip to content

refactor: finalize Search router and delete legacy SearchList shell#95370

Merged
mountiny merged 5 commits into
Expensify:mainfrom
callstack-internal:perf/search-s6-router-finalization
Jul 6, 2026
Merged

refactor: finalize Search router and delete legacy SearchList shell#95370
mountiny merged 5 commits into
Expensify:mainfrom
callstack-internal:perf/search-s6-router-finalization

Conversation

@BartekObudzinski

@BartekObudzinski BartekObudzinski commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

This finalizes the Search list rendering refactor. Previously most search types rendered through a single large SearchList component backed by a getListItem factory and one shared renderItem with a wide prop bundle. Dedicated views composed directly over BaseSearchList were already introduced for flat-expense, grouped-expense, chat, expense-report, and task.

This change:

  • Routes the remaining transaction types (invoice and trip) through the existing ExpenseFlatSearchView and ExpenseGroupedSearchView. Invoice and trip use the same row components (TransactionListItem / TransactionGroupListItem) and the same getSections data path as expense, so the router predicate is broadened from type === EXPENSE to "type is not chat, task, or expense-report", which matches how getSections itself dispatches.
  • Preserves the exit-animation behavior: only expense rows animate their exit, so invoice and trip do not, matching the previous shouldAnimate={type === CONST.SEARCH.DATA_TYPES.EXPENSE} gate.
  • Extracts the shared view props into a single commonViewProps object spread across the five view invocations, removing the near-duplicate prop lists at the call sites.
  • Moves isTransactionMatchWithGroupItem from the deleted component into SearchUIUtils.
  • Deletes the now-unused SearchList component, the getListItem factory, and the shared renderItem. BaseSearchList is intentionally KEPT: it is the shared FlashList layer that every view renders directly.
  • Rebalances the eslint-seatbelt baseline to reflect isTransactionMatchWithGroupItem moving files. This is net zero: the deleted component's entry is removed and SearchUIUtils increases by the same count.

Result: <Search> is now a pure router over the five dedicated views, with no shared list component. Behavior is preserved for every search type.

Fixed Issues

$ #95380
PROPOSAL:

Tests

  1. Open Search and switch the type filter across Expenses, Invoices, Trips, Chats, Tasks, and Reports, and verify each list renders correctly.
  2. For Invoices and Trips specifically, verify rows render as before and do NOT animate on exit, and that row tap, multi-select, and long-press work.
  3. For Expenses with a groupBy (for example by category or by card), verify grouped rendering, sticky headers on wide web, and expand/collapse all work.
  4. Verify row tap, select-all, keyboard navigation (web), and horizontal scroll behave as before across every type.
  • Verify that no errors appear in the JS console

Offline tests

N/A

QA Steps

Same as tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Screen.Recording.2026-07-06.at.12.51.20.mov
Android: mWeb Chrome
iOS: Native
Screen.Recording.2026-07-06.at.12.45.24.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-07-06.at.12.37.54.mov

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

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.

Files with missing lines Coverage Δ
src/components/Search/ChatSearchView.tsx 100.00% <ø> (ø)
src/components/Search/ExpenseFlatSearchView.tsx 100.00% <100.00%> (ø)
src/components/Search/ExpenseReportSearchView.tsx 100.00% <ø> (ø)
src/components/Search/TaskSearchView.tsx 100.00% <ø> (ø)
.../components/Search/hooks/useSearchListViewState.ts 80.00% <0.00%> (ø)
src/components/Search/ExpenseGroupedSearchView.tsx 60.21% <25.00%> (-0.90%) ⬇️
src/components/Search/index.tsx 0.74% <0.00%> (+0.01%) ⬆️
src/libs/SearchUIUtils.ts 62.68% <5.26%> (-1.08%) ⬇️
... and 34 files with indirect coverage changes

@BartekObudzinski BartekObudzinski marked this pull request as ready for review July 6, 2026 11:15
@BartekObudzinski BartekObudzinski requested review from a team as code owners July 6, 2026 11:15
@melvin-bot melvin-bot Bot requested review from a team, ikevin127 and trjExpensify and removed request for a team July 6, 2026 11:15
@melvin-bot

melvin-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

@ikevin127 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]

@BartekObudzinski

Copy link
Copy Markdown
Contributor Author

@codex review

@melvin-bot melvin-bot Bot requested review from mountiny and suneox and removed request for a team July 6, 2026 11:15
@melvin-bot

melvin-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

@suneox @mountiny One of you needs to 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]

@mountiny mountiny requested review from daledah and removed request for ikevin127 and suneox July 6, 2026 11:17
@mountiny

mountiny commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 719bc33240

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@MelvinBot

Copy link
Copy Markdown
Contributor

Review — LGTM ✅

This is a well-scoped, behavior-preserving refactor. I traced the two behavioral-risk areas (the broadened router predicate and the exit-animation gate) and both hold up. No blocking issues.

What I verified

1. The broadened router predicate is correct. index.tsx:1056 routes anything that isn't CHAT/TASK/EXPENSE_REPORT through the flat/grouped views. That exactly mirrors getSections, which branches off chat/task/expense-report and falls everything else (expense, invoice, trip) through to getTransactionsSections. So invoice/trip genuinely produce TransactionListItemType/TransactionGroupListItemType rows — the views can render them. DATA_TYPES has exactly 6 members, so the predicate is exhaustive.

2. Exit-animation behavior is preserved for invoice/trip. Legacy passed shouldAnimate={type === EXPENSE} (false for invoice/trip). The new gates — ExpenseFlatSearchView and ExpenseGroupedSearchView — both gate on type === EXPENSE && index < …, so invoice/trip still don't animate. Matches.

Other things I checked (all fine)
  • policyTags removal is safe. The deleted SearchList drilled policyTags to rows, and invoice/trip previously routed through it — but no row component actually consumes a policyTags prop. The only real consumer, TransactionGroupListExpanded, subscribes via its own useOnyx. So the drilled prop was dead; removing the useOnyx from <Search> is a net win.
  • onEndReachedThreshold preserved. Legacy set 0.75; all five views hardcode onEndReachedThreshold={0.75}, so invoice/trip keep the same pagination trigger.
  • shouldPreventLongPressRow was isChat || isTask = false for invoice/trip, and the flat/grouped views (already used by expense) enable long-press — consistent.
  • eslint-seatbelt rebalance is net-zero and accurate. isTransactionMatchWithGroupItem has exactly 7 group-item as casts (card/category/merchant/month/week/year/quarter), matching the −7 on the deleted file and +7 on SearchUIUtils.
  • No stale references to the deleted module, getListItem, or the removed ListItemType export anywhere in src/tests.
  • Perf-guard coverage isn't lost. Deleting SearchListRenderCountTest is fine — each dedicated view test (ExpenseFlatSearchViewTest, etc.) already carries its own Profiler render-count guard, and the flat-view test now covers the invoice/trip path.

Optional nits (non-blocking)

  • The extras type CommonSearchViewProps & {isAttendeesEnabledForMovingPolicy?: boolean; nonPersonalAndWorkspaceCards?: CardList} is duplicated verbatim in both ExpenseFlatSearchView and ExpenseGroupedSearchView. Could be a shared ExpenseSearchViewProps alias.
  • Since invoice/trip now share the expense views, it's worth a quick manual smoke of an invoice and a trip search (flat + grouped) — the automated coverage exercises the expense path, but a visual confirm on the two newly-routed types would close the loop.

Nice cleanup — <Search> reads much better as a pure router now.

@daledah

daledah commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-07-06.at.18.43.42.mov

/** Imperative handle for highlight-driven scrolling, set by the router. */
ref?: ForwardedRef<SearchListHandle>;
};
type ExpenseGroupedSearchViewProps = CommonSearchViewProps & {isAttendeesEnabledForMovingPolicy?: boolean; nonPersonalAndWorkspaceCards?: CardList};

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.

NIT: The extended props type CommonSearchViewProps & {isAttendeesEnabledForMovingPolicy?: boolean; nonPersonalAndWorkspaceCards?: CardList} is duplicated in both ExpenseFlatSearchView.tsx and ExpenseGroupedSearchView.tsx. I think we should extract a shared props in searchViewProps.ts

@daledah

daledah commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@BartekObudzinski please fill Tests section

@daledah daledah left a comment

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.

One minor nit about the duplicated but that's non-blocking. Code changes LGTM. Nice cleanup!

@BartekObudzinski

Copy link
Copy Markdown
Contributor Author

@daledah updated!

@mountiny mountiny left a comment

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.

Great, thank you!

@mountiny mountiny merged commit 0f4b2c7 into Expensify:main Jul 6, 2026
35 checks passed
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

OSBotify commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants