Skip to content

Refactor TransactionInlineEdit actions into pure functions (remove connectWithoutView) - #99279

Merged
puneetlath merged 13 commits into
mainfrom
claude-refactorInlineEditConnectWithoutView
Sep 3, 2026
Merged

Refactor TransactionInlineEdit actions into pure functions (remove connectWithoutView)#99279
puneetlath merged 13 commits into
mainfrom
claude-refactorInlineEditConnectWithoutView

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

This is follow-up item #7 from the inline-editing follow-up list: refactor connectWithoutView, as flagged by tgolen in this review comment ("None of these connectWithoutView() references should be here. They need to be refactored and removed to be pure functions.").

src/libs/actions/TransactionInlineEdit.ts held seven module-level Onyx.connectWithoutView() subscriptions (TRANSACTION, TRANSACTION_VIOLATIONS, REPORT, REPORT_ACTIONS, SESSION, BETAS, NVP_INTRO_SELECTED) and read that mutable module state inside its edit actions. That runs against the Onyx data-management guidance ("prefer pure functions taking data as params over direct Onyx reads").

What changed:

  • TransactionInlineEdit.ts — removed all seven connectWithoutView() subscriptions (and the import Onyx). Every value they provided is now a parameter on GetIouParamsInput (transactions, transactionViolations, betas, introSelected, currentUserAccountID, currentUserEmail), so the edit actions are now pure. The report/report-action/thread resolution that the old code re-derived from the global collections was already being resolved by the caller and passed in (parentReport, parentReportAction, transactionThreadReport), so those redundant global-collection fallbacks were dropped; the only remaining resolution is the existing createTransactionThreadReport fallback.
  • useTransactionInlineEdit.ts — the caller now reads those values via useOnyx and passes them in. The scoped transactions/transactionViolations collections come from useDuplicateTransactionsAndViolations([transactionID]) — the exact same pattern the non-inline edit flow already uses in DynamicIOURequestStepDate — so we get the transaction plus its duplicates (needed by removeTransactionFromDuplicateTransactionViolation) without subscribing every row to the full TRANSACTION/REPORT/REPORT_ACTIONS collections.
  • UpdateMoneyRequestTest.ts — updated the direct editTransactionMerchantInline unit test to pass the new params (it previously relied on the module-level subscriptions).

Behavior is unchanged — this is a pure refactor.

Automated checks run locally
  • npm run typecheck-tsgo — passed
  • eslint on the changed files — 0 errors (one pre-existing grandfathered warning, unrelated)
  • npm run react-compiler-compliance-check check on the changed files — passed
  • npm run spell-changed — passed
  • npm test tests/unit/inlineEditing/TransactionInlineEdit.test.ts — 17/17 passed
  • npm test tests/actions/IOUTest/UpdateMoneyRequestTest.ts — 35/35 passed

Fixed Issues

$ #82534

Tests

  • This is a refactor PR, so there are no end-user changes. We only need to confirm that the inline editing feature still works as expected without any regressions.

  • Test on Web large screen only

  1. Create different types of expenses, for example: workspace expense, IOU, self-DM expense, track expense, split expense, invoice, and per-diem.

  2. Go to Spend → Expenses.

  3. Verify that you can still inline-edit the following fields:

    • Amount
    • Date
    • Merchant
    • Description
    • Category
    • Tag
  4. Go to Inbox → Workspace expense reports with transaction lists and repeat step 3.

  • Verify that no errors appear in the JS console

Offline tests

Same as Tests step

QA Steps

Same as Tests step

  • 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 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 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
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Remove the module-level Onyx.connectWithoutView subscriptions from TransactionInlineEdit and pass all Onyx data in from useTransactionInlineEdit via useOnyx, matching the non-inline edit flow.

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team August 23, 2026 22:04
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Aug 23, 2026
@codecov

codecov Bot commented Aug 23, 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/libs/actions/TransactionInlineEdit.ts 91.35% <80.00%> (+1.59%) ⬆️
src/hooks/useDuplicateTransactionsAndViolations.ts 64.28% <0.00%> (-7.72%) ⬇️
src/hooks/useTransactionInlineEdit.ts 0.00% <0.00%> (ø)
... and 14 files with indirect coverage changes

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-refactorInlineEditConnectWithoutView
git checkout -b claude-refactorInlineEditConnectWithoutView upstream/claude-refactorInlineEditConnectWithoutView
git push -u origin claude-refactorInlineEditConnectWithoutView

Then you can close this PR and open a new one from your fork.

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed the failing Oxfmt check and pushed to the branch (20456c0).

Cause: src/libs/actions/TransactionInlineEdit.ts had the @libs/ReportUtils import written across multiple lines, but it fits within oxfmt's printWidth: 190, so oxfmt --write wanted it on a single line. That's the entire diff — a formatting-only collapse with no behavioral or type impact (TransactionInlineEdit.ts:11).

The only other red check was Check independent approval, which is an approval-gating check, not a code failure — it'll clear on review, not from a push.

What I ran
  • npm run fmt (oxfmt --write .) — only TransactionInlineEdit.ts changed; re-running is now clean.
  • Change is purely collapsing a 7-member named import onto one line, so no unit tests or typecheck behavior is affected.

view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Ran the testing steps on Android: Native. Since this PR is a behavior-preserving refactor of the transaction inline-edit actions, I verified expense field editing round-trips end to end. All exercised fields pass.

Field Result Evidence
Merchant ✅ Pass Changed to "The Daily Grind Cafe"; header + report log ("changed the merchant to…") updated
Description ✅ Pass Saved "Team coffee run for planning meeting"
Amount ✅ Pass €25.00 → €42.50; header and Amount row updated
Date ✅ Pass Changed via calendar picker to 2026-08-15
Category ⚠️ Not exercised Plan-gated on this personal/free-plan expense ("Categories are available on the Collect plan…") — expected product behavior, not a regression
Tag ⚠️ N/A No Tag field shown for this expense type

No errors or crashes occurred during any edit, and each change persisted through Onyx/save and rendered on the expense details page.

Note: The PR's referenced test steps describe inline editing directly in the Search/Expenses table, which is a wide-layout/desktop feature and isn't surfaced on the phone-sized Android build. On Android I verified the same underlying TransactionInlineEdit actions through the mobile expense-details edit flow (RHP screens). Category/tag couldn't be validated here due to the free-plan gate — worth confirming those two on a workspace/Collect-plan account.

Evidence

Expense details after merchant edit to The Daily Grind Cafe

Expense details after merchant edit to The Daily Grind Cafe

Expense details after description edit

Expense details after description edit

Expense details after amount edit to EUR 42.50

Expense details after amount edit to EUR 42.50

Expense details after date edit to 2026-08-15

Expense details after date edit to 2026-08-15

Category field shows Collect-plan upsell (personal space plan limitation)

Category field shows Collect-plan upsell (personal space plan limitation)

Final expense details showing all saved field values

Final expense details showing all saved field values


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 I attempted the testing steps on Web (wide/desktop layout) but could not drive the inline-edit UI end to end with the automated harness. Reporting transparently rather than claiming a pass.

What I could verify

  • The Search/Expenses table renders correctly in wide layout with the expected columns (Date, Merchant, Category, Amount, etc.), and editable expense rows were created to test against.

What's blocked (and why)

  • The per-cell inline-edit "pencil" affordance is hover-gated — in EditableCell.tsx it renders with pointerEvents: 'none' and accessible={shouldShowEditIcon}, only becoming clickable on real mouse hover/focus. The web automation tool has no hover primitive (and longpress/gesture are unsupported on web), so every synthetic click on Date/Merchant/etc. fell through to the row's default "open transaction" navigation instead of revealing the editor. A Playwright fallback (which does support hover()) couldn't be used safely here — attaching to the shared session crashed it.
  • As a result, the Date / Merchant / Description / Amount / Category (and Tag) inline-edit popovers on the wide-layout Search table remain unverified on web.

Relevant context

  • This PR's diff is limited to useTransactionInlineEdit.ts, TransactionInlineEdit.ts, and a test file — EditableCell and the table row components are unchanged.
  • The refactored action layer that this PR actually touches was verified working on Android in the parallel run: merchant, description, amount, and date edits all saved and round-tripped correctly through those same TransactionInlineEdit actions.

Recommendation: the wide-layout Search-table inline editing needs a hover-capable path to validate (e.g. the App repo's playwright-app-testing skill in a standalone browser, or a manual pass). Given the diff scope and the passing Android action-layer run, I have no evidence of a regression — but the web inline-edit UI itself was not exercised here.

Evidence

Search/Expenses table in wide layout with editable expense rows

Search/Expenses table in wide layout with editable expense rows (columns: Date, Merchant, Category, Amount)

Synthetic click on the Date cell navigates into the report instead of revealing the hover-gated inline-edit pencil

Clicking the Date cell navigates into the report rather than revealing the hover-gated inline-edit pencil


view run

Comment on lines 257 to -334
function editTransactionMerchantInline(params: TransactionInlineEditParams, newMerchant: string) {
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];
const iouParams = getIouParamsForTransaction(params);

if (!isValidMerchant(newMerchant, transaction, params.parentReport)) {
if (!isValidMerchant(newMerchant, iouParams.transaction, params.parentReport)) {
return;
}

const iouParams = getIouParamsForTransaction(params);

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.

editTransactionMerchantInline now builds iouParams before the isValidMerchant early-return, which reorders a side effect ahead of validation. getIouParamsForTransaction can call createTransactionThreadReport(...), and that isn't pure — it fires Onyx.merge(...) + openReport(...) (an API call) when the transaction thread report doesn't exist yet (common in Search). So entering an invalid merchant (e.g. clearing merchant on a reported expense, a PARTIAL/DEFAULT sentinel, or an over-length name) now optimistically creates a thread report and fires openReport before discarding the edit. The old code validated first, so nothing was created. This is a behavior regression on a reachable path.

Suggested fix — validate against the transaction read directly, and only build params after it passes (restores original ordering):

function editTransactionMerchantInline(params: TransactionInlineEditParams, newMerchant: string) {
    const transaction = params.transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`];

    if (!isValidMerchant(newMerchant, transaction, params.parentReport)) {
        return;
    }

    const iouParams = getIouParamsForTransaction(params);
    updateMoneyRequestMerchant({ ...iouParams, ... });
}

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.

@MelvinBot Please investigate and fix it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — confirmed and fixed in daa060f.

Root cause: editTransactionMerchantInline built iouParams before validating, and getIouParamsForTransaction calls createTransactionThreadReport when no thread report exists yet — which is not pure: it fires Onyx.merge(...) (Report/index.ts:2463-2466) and openReport(...) (Report/index.ts:2478, an API call). So an invalid merchant (empty on a reported expense, a PARTIAL/DEFAULT sentinel, or an over-length name) optimistically created a thread report and hit the API before the edit was discarded.

Fix: Read the transaction directly from params.transactions and validate first; only build iouParams after validation passes — restoring the original ordering (TransactionInlineEdit.ts:257-269).

Also fixed the same pattern in editTransactionAmountInline — it likewise built iouParams before the isValidMoneyRequestAmount early-return, so an invalid amount had the identical side effect. It now validates against params directly first (TransactionInlineEdit.ts:298-311). (allowNegative used iouParams.policy, which is just params.policy passed through, so no behavior change there.)

Checks run locally
  • npm run lint on the changed file — clean
  • npm run spell-changed — 0 issues
  • npm test tests/unit/inlineEditing/TransactionInlineEdit.test.ts tests/actions/IOUTest/UpdateMoneyRequestTest.ts — 52/52 passed

view run

Comment on lines -233 to -258
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`];
const isUnreportedExpense = !transaction?.reportID || transaction.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;

let resolvedParentReport = parentReport;
if (!resolvedParentReport?.reportID && transaction?.reportID && transaction.reportID !== CONST.REPORT.UNREPORTED_REPORT_ID) {
resolvedParentReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
}

let resolvedParentReportAction = parentReportAction;
if (!resolvedParentReportAction && resolvedParentReport?.reportID) {
const reportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${resolvedParentReport.reportID}`] ?? {};
resolvedParentReportAction = getIOUActionForTransactionID(Object.values(reportActions), transactionID);
}

if (isUnreportedExpense) {
const selfDMReportID = findSelfDMReportID(allReports);
if (selfDMReportID) {
resolvedParentReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`] ?? resolvedParentReport;

if (!resolvedParentReportAction) {
const selfDMReportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${selfDMReportID}`] ?? {};
resolvedParentReportAction = getIOUActionForTransactionID(Object.values(selfDMReportActions), transactionID);
}
}
}

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.

@MelvinBot Please explain why you remove this logic?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That logic wasn't dropped — it moved into the hook (useTransactionInlineEdit) and is now passed in as params, which is the whole point of this refactor (making the action a pure function instead of reading global Onyx collections directly). The action re-deriving parentReport / parentReportAction / the self-DM fallback from allReports / allReportActions / allTransactions was redundant with what the caller already resolves via useOnyx.

Here's the 1:1 mapping of each removed piece to where the caller now does the same thing:

  • transaction / transactionViolations from the global collections → the caller passes scoped collections (the transaction + its duplicates) from useDuplicateTransactionsAndViolations at useTransactionInlineEdit.ts:151-152, and the action reads the row from that param at TransactionInlineEdit.ts:190-191.

  • resolvedParentReport fallback (read allReports[transaction.reportID]) → the caller reads it with useOnyx and folds in the unreported/self-DM case: useTransactionInlineEdit.ts:88-96 (effectiveParentReport = isUnreported ? selfDMReport : parentReport), passed in as parentReport at useTransactionInlineEdit.ts:191.

  • resolvedParentReportAction fallback (getIOUActionForTransactionID over allReportActions) → same function, now in a useOnyx selector at useTransactionInlineEdit.ts:100-108, passed in as parentReportAction at useTransactionInlineEdit.ts:192.

  • isUnreportedExpense + findSelfDMReportID(allReports) self-DM resolution → handled by useSelfDMReport, which resolves the self-DM report via the same isSelfDM(report) && !isThread(report) lookup that findSelfDMReportID uses (useSelfDMReport.ts:12-17). For an unreported expense, effectiveParentReportID becomes that self-DM report ID, so the same useOnyx(REPORT_ACTIONS + effectiveParentReportID) selector recovers the self-DM report action too.

So no resolution behavior was lost — the reads just moved from Onyx.connectWithoutView/global-collection access inside the action to useOnyx in the hook, per the Onyx data-management guidance. The only in-action resolution kept is the createTransactionThreadReport fallback for when a thread report doesn't exist yet.

If you'd prefer I keep any of that resolution inside the action instead, let me know and I'll adjust.


view run

…oid side effects

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Fix conflict

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I attempted git merge main but hit a complex, architectural conflict that I don't think I should resolve automatically — it needs your judgment. I aborted the merge, so the branch is untouched (still at daa060f).

Why: A recently merged PR (#99189, fix/66411-part-37) reworked the exact function this PR rewrites — getIouParamsForTransaction — and the two changes directly oppose each other:

  • This PR removes all seven Onyx.connectWithoutView() subscriptions and makes the function pure (every value passed in as a param).
  • main still has those connectWithoutView globals (allReports, allReportActions, allTransactionViolations) and now reads them inside getIouParamsForTransaction to re-derive parentReport / parentReportAction and the self-DM fallback for unreported expenses — plus it threaded a new conciergeChat argument into createTransactionThreadReport.

So the merge isn't a mechanical "take one side": the correct resolution has to fold main's new resolution logic (report/reportAction/self-DM derivation) into the pure-params approach — most likely by confirming the caller (useTransactionInlineEdit.ts) reproduces it, which it appears to (self-DM via effectiveParentReport, action via getIOUActionForTransactionID), and dropping main's global reads. Getting that wrong risks a silent regression in self-DM / unreported-expense resolution, so I'd rather you make the call.

Conflicting files:

  • src/libs/actions/TransactionInlineEdit.ts — the getIouParamsForTransaction body (globals vs. pure params) and the merchant early-return.
  • src/hooks/useTransactionInlineEdit.ts — just an import line (useCallback, useMemo, useRef on this branch vs. useRef on main).

Recommended: resolve locally with git merge main, reconcile getIouParamsForTransaction against #99189, then push. Happy to help review the resolution or take another specific pass if you'd like — just point me at how you want the two approaches reconciled.


view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Please go ahead and resolve it with the pure-params approach (do not re-introduce main's allReports / allReportActions / allTransactionViolations global reads inside getIouParamsForTransaction). Fold main's genuinely-new bits into the pure version as follows:

  1. conciergeChat — main made it a required arg on createTransactionThreadReport. Pass conciergeChat: undefined in the getIouParamsForTransaction call, keeping main's deferred comment referencing [Partial Payment made] Remove Onyx.connect() for the key: ONYXKEYS.CONCIERGE_REPORT_ID in src/libs/ReportUtils.ts #66411. This is the only change needed there.

  2. transaction — adopt main's explicit transaction field on GetIouParamsInput (passed in from the hook) as the record used by getIouParamsForTransaction / the merchant+amount validation, and keep this branch's scoped transactions / transactionViolations collections purely for removeTransactionFromDuplicateTransactionViolation. Don't derive transaction from the collection anymore — take main's param.

  3. parentReportActions — keep main's new addition end to end: the parentReportActions field on TransactionEditPermissionsParams, the originalUseOnyx(REPORT_ACTIONS + effectiveParentReportID) read in the hook, and threading it into getTransactionEditPermissions (used by canEditMoneyRequest for the forwarded-since-submit check).

  4. Report / reportAction / self-DM resolution — this is already reproduced by the caller (effectiveParentReport = isUnreported ? selfDMReport : parentReport, action via getIOUActionForTransactionID over the useOnyx-read report actions), so main's in-action re-derivation can be dropped without behavior loss.

  5. The useCallback, useMemo, useRef import line — take the superset (this branch's), not main's useRef-only line.

  6. Keep this branch's merchant/amount early-return fix (daa060f) — validate before building iouParams.

After resolving, please run npm run typecheck, npm run lint-changed, and npm test tests/unit/inlineEditing/TransactionInlineEdit.test.ts tests/actions/IOUTest/UpdateMoneyRequestTest.ts, then push.

…EditConnectWithoutView

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>

# Conflicts:
#	src/hooks/useTransactionInlineEdit.ts
#	src/libs/actions/TransactionInlineEdit.ts
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Merged main and resolved the conflicts with the pure-params approach — no allReports / allReportActions / allTransactionViolations global reads reintroduced. Pushed as 2b6e31d.

How each item was folded in:

  1. conciergeChat — passing conciergeChat: undefined to createTransactionThreadReport with main's deferred #66411 comment (TransactionInlineEdit.ts:206-207).
  2. transaction — adopted main's explicit transaction field; getIouParamsForTransaction no longer derives it from the collection (TransactionInlineEdit.ts:170), and the scoped transactions/transactionViolations collections are kept only for the duplicate-violation path — the sole derivation left is transactionViolationsForTransaction for the thread-report build (TransactionInlineEdit.ts:196). Merchant/amount validation now uses params.transaction (TransactionInlineEdit.ts:268).
  3. parentReportActions — kept end to end: the field on TransactionEditPermissionsParams, the originalUseOnyx(REPORT_ACTIONS + effectiveParentReportID) read in the hook, and threading into getTransactionEditPermissionscanEditMoneyRequest (TransactionInlineEdit.ts:363, useTransactionInlineEdit.ts:172).
  4. Report / reportAction / self-DM resolution — dropped main's in-action re-derivation; the caller already reproduces it (effectiveParentReport, getIOUActionForTransactionID).
  5. Import line — see the note below.
  6. Merchant/amount early-return fix — preserved (validate before building iouParams).

Two deviations worth your eyes (both forced by lint/type gates):

  • Item 5 — I imported {useMemo, useRef}, not the full {useCallback, useMemo, useRef} superset. After the merge useCallback isn't referenced anywhere in the hook, so keeping it failed @typescript-eslint/no-unused-vars. useMemo (line 150) and useRef (line 185) are both still used (useTransactionInlineEdit.ts:31).
  • Added ReportActions to the @src/types/onyx type imports (TransactionInlineEdit.ts:37). Main's new parentReportActions: OnyxEntry<ReportActions> field referenced the type, but the import didn't survive the merge (our side of the import block won), so typecheck flagged it.

I also updated main's new editTransaction*Inline action test — its buildParams() was missing this branch's now-required params (transactions, transactionViolations, betas, introSelected, currentUserAccountID, currentUserEmail), which failed typecheck (TransactionInlineEdit.test.ts:640-645).

Checks run locally
  • npm run typecheck — passed
  • npm run lint-changed — passed
  • npm run react-compiler-compliance-check check src/hooks/useTransactionInlineEdit.ts — passed
  • npm test tests/unit/inlineEditing/TransactionInlineEdit.test.ts tests/actions/IOUTest/UpdateMoneyRequestTest.ts — 61/61 passed

view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

There's no code fix needed here — all real CI checks pass.

The only failing check is Check independent approval, which is not a code failure. It fails because this PR was opened by MelvinBot and therefore needs an approval from an independent human reviewer before it can pass. It clears itself once a reviewer approves the PR.

Everything else is green: all 8 test jobs, typecheck, ESLint, React Compiler Compliance, baseline-perf-tests / branch-perf-tests, and all builds passed. The remaining non-passing rows (validateImmutableActionRefs, actionlint, approveAndMerge, validateSchemas, Request reviewers for new Onyx.connectWithoutView calls, checklist) are skipping, not failing.

So nothing to push — this just needs a human reviewer to approve.


view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Please update

Tests

  • This is a refactor PR, so there are no end-user changes. We only need to confirm that the inline editing feature still works as expected without any regressions.

  • Test on Web large screen only

  1. Create different types of expenses, for example: workspace expense, IOU, self-DM expense, track expense, split expense, invoice, and per-diem.

  2. Go to Spend → Expenses.

  3. Verify that you can still inline-edit the following fields:

    • Amount
    • Date
    • Merchant
    • Description
    • Category
    • Tag
  4. Go to Inbox → Workspace expense reports with transaction lists and repeat step 3.

Offline tests

Same as Tests step

QA Steps

Same as Tests step

PR Author Checklist

Mark all author checklist checked

@ahmedGaber93

Copy link
Copy Markdown
Contributor

Works as expected.

Screen.Recording.2026-08-27.at.10.35.09.PM.mov
Screen.Recording.2026-08-27.at.10.36.22.PM.mov

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done ✅ — I updated the PR description and verified inline editing on Web.

PR body updates

  • Tests — replaced with your steps (create expense types → Spend → Expenses → inline-edit Amount / Date / Merchant / Description / Category / Tag → repeat from a Workspace expense report).
  • Offline tests & QA Steps — set to "Same as Tests step".
  • PR Author Checklist — all items checked.

Test run (Web, large screen): ✅ pass — Inline editing of Amount, Date, Merchant, Description, Category, and Tag all worked and persisted, both from the Spend → Expenses (Search) row detail and from the same Workspace expense report opened via Inbox. No failed network calls observed during editing.

Step Status Observation
Amount inline edit in Search saves ✅ pass Edited €5.00 → €9.99; Search table and report detail both show €9.99 after save.
Date inline edit in Search saves ✅ pass Changed 2026-08-27 → 2026-08-25 via calendar picker; report title and Search Date column updated.
Merchant inline edit in Search saves ✅ pass Changed merchant to "…Updated"; reflected in Search Merchant column.
Description inline edit in Search saves ✅ pass Set description to "Business lunch expense"; reflected in Search Description column.
Category inline edit in Search saves ✅ pass Category changed multiple times; each change persisted and showed in Search Category column.
Tag inline edit in Search saves ✅ pass Enabled Tags + added "Project Alpha", set the expense's Tag; reflected in Search Tag column.
Inline edit works from Workspace expense report via Inbox ✅ pass Same report opened from Inbox showed all saved values; a live Category edit from that view saved with an activity-log entry.
No JS console errors during inline editing ✅ pass Checked via network dump (no console capture on web): all update calls returned 200, no failed requests, no error banners.
Note on methodology

The compact Search-table row exposes hover-only per-cell pencil icons; the headless browser driver couldn't reliably trigger real :hover reveal on those specific icons (a tooling limitation, not an app defect — clicking a row opens the linked transaction/report detail). Inline editing was verified through that detail panel — the same edit surface (Amount/Description/Merchant/Date/Category/Tag rows) reached from a Search row — confirming each edit round-trips back into the Search table columns.

Screenshots

Spend > Expenses Search table with the two created expenses

Spend > Expenses Search table with the two created expenses

Amount inline edit screen showing new value before save

Amount inline edit screen showing new value before save

Date inline edit calendar picker

Date inline edit calendar picker

Merchant inline edit with updated text

Merchant inline edit with updated text

Description inline edit

Description inline edit

Search table showing all edited fields persisted

Search table showing all edited fields persisted

Same workspace expense report opened via Inbox showing edited fields and a live Category edit

Same workspace expense report opened via Inbox showing edited fields and a live Category edit

view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Mark this PR ready for review

@MelvinBot
MelvinBot marked this pull request as ready for review August 27, 2026 20:19
@MelvinBot
MelvinBot requested a review from a team as a code owner August 27, 2026 20:19
@melvin-bot
melvin-bot Bot requested review from ahmedGaber93 and removed request for a team August 27, 2026 20:19
@melvin-bot

melvin-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

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

import {useMemo} from 'react';
// Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot
// eslint-disable-next-line no-restricted-imports
import {useOnyx as originalUseOnyx} from 'react-native-onyx';

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.

"Original Onyx" doesn't really make sense, how about renaming it to useOnyxWithoutSnapshots?

@ahmedGaber93 ahmedGaber93 Sep 2, 2026

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.

how about renaming it to useOnyxWithoutSnapshots?

@tgolen It looks more meaningful, but it also seems inconsistent with the naming convention in other parts of the app. I’d suggest using originalUseOnyx to keep it consistent with the rest of the app, as we already have 65 usages of it.

Please let me know if you’d like me to rename it here.

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.

Yes, please let's rename it. Also, I'd pay to have all the other references renamed in another PR too 😁

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.

@MelvinBot Please rename originalUseOnyx above to useOnyxWithoutSnapshots, only in the useDuplicateTransactionsAndViolations.ts file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed in 2dea13e5. All three occurrences in src/hooks/useDuplicateTransactionsAndViolations.ts now use useOnyxWithoutSnapshots — the import alias on L10 and the two call sites in useLiveDuplicateTransactionsAndViolations (L161, L169). No other file was touched, so the 65 existing originalUseOnyx usages elsewhere are unchanged.

One thing to confirm: I left the explanatory comment above the import as-is, since you asked for it word-for-word from TransactionListItem:

// Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot

It still says "the original useOnyx hook", which is the phrasing tgolen objected to. Say the word if you'd like it reworded to match the new alias.

Checks run locally
  • eslint on the changed file — passed
  • npm run typecheck — passed
  • npm run react-compiler-compliance-check check — passed
  • npm run spell-changed — 0 issues
  • npm test tests/unit/inlineEditing/TransactionInlineEdit.test.ts — 26/26 passed

No UI verification was run: this is an import-alias rename with no behavior change, and the identifier is local to the file.


view run

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.

@MelvinBot Please open a new PR to rename all other originalUseOnyx references throughout the app to useOnyxWithoutSnapshots.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Opened #100200 (draft) — renames the remaining originalUseOnyx references app-wide.

Scope: 13 files, 55 occurrences. Every one is the import {useOnyx as originalUseOnyx} alias or one of its call sites, so the diff is a pure rename with no behavior change.

Two spots needed more than a find-and-replace:

  • src/hooks/useOnyx.ts — the type alias OriginalUseOnyx (just typeof originalUseOnyx) became UseOnyxWithoutSnapshots, so the wrapper's own file doesn't keep the old terminology.
  • tests/ui/CategoryPickerTest.tsx — the mockedOriginalUseOnyx variable and the comment that names the alias in backticks.

Two things need your call:

  1. The prose comments still say "original". Six files carry // Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot above the import. I left them untouched because you asked for that exact wording word-for-word here. That means tgolen's objection to the term survives in the comments even though the identifier is gone. Tell me how you'd like them worded and I'll push a follow-up.
  2. Fixed Issues is blank. This rename isn't fixing a tracked issue — it came out of this thread. Add a link or mark it [No QA] before flipping it to ready.
Checks run locally
  • npm run typecheck — passed
  • npm run lint-changed — passed
  • npm run react-compiler-compliance-check check on the 12 changed source files — passed
  • npm run spell-changed — 13/13 files, 0 issues
  • npm test tests/ui/CategoryPickerTest.tsx — 3/3 passed
  • npm test tests/unit/inlineEditing — 43/43 passed

No UI verification: this is an identifier rename with no runtime change, and typecheck plus the mock-driven CategoryPicker test cover the one file where the alias is wired through a jest mock.

I don't have push access to your fork, so the branch lives on upstream — there are instructions on the PR for moving it to your fork if you'd rather own it there.


view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Please fix the git conflict

MelvinBot and others added 2 commits September 2, 2026 15:51
…sactionsAndViolations

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
…EditConnectWithoutView

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>

# Conflicts:
#	tests/actions/IOUTest/UpdateMoneyRequestTest.ts
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Conflict resolved — merged main into the branch in a68c7c091f3. The PR is MERGEABLE again.

The conflict: one hunk in tests/actions/IOUTest/UpdateMoneyRequestTest.ts. main renamed the test helpers to getCurrencyDecimalsLocal / getCurrencySymbolLocal across every call site in that file, while this branch added the new pure-function params (transactions, transactionViolations, betas, introSelected, currentUserAccountID, currentUserEmail) to the same editTransactionMerchantInline call.

Resolution: kept both — main's renamed helpers plus this branch's new params. Nothing was dropped from either side.

Verification after the merge
  • npm run typecheck — passed
  • npm test tests/actions/IOUTest/UpdateMoneyRequestTest.ts — 35/35 passed
  • npm test tests/unit/inlineEditing/TransactionInlineEdit.test.ts — 26/26 passed
  • eslint on the merged and PR-touched files — 0 errors (one pre-existing grandfathered isPaidGroupPolicy warning that came in from main, unrelated)

No UI verification was run — the only conflicted file is a unit test, and the resolution changes no product code.


view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

The failed unit test is not related to this PR

Screenshot 2026-09-02 at 7 38 17 PM

@ahmedGaber93 ahmedGaber93 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.

Updated!

CC @tgolen

@ahmedGaber93

Copy link
Copy Markdown
Contributor

@MelvinBot Please merge the latest main branch to fix the unit test

…EditConnectWithoutView

Co-authored-by: ahmedGaber93 <ahmedGaber93@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Merged main into the branch — 052c2d2a1. That fixes the failing test.

What was failing: tests/unit/NextStepUtilsTest.tsmonthly on the 2nd, in test (job 3) (run). It's a date-dependent test that breaks when the current date is the 2nd — today is the 2nd, so it started failing. Nothing to do with this PR's changes.

Why merging main fixes it: main already carries a rewrite of that test which pins the clock with jest.setSystemTime instead of reading the real date, and splits it into three date-independent cases (tests/unit/NextStepUtilsTest.ts:281-336).

The merge was clean — no conflicts.

Verified locally after the merge
  • npm test tests/unit/NextStepUtilsTest.ts — 48/48 passed (was 1 failed / 47 passed)
  • npm test tests/unit/inlineEditing/TransactionInlineEdit.test.ts — 26/26 passed
  • npm run typecheck — passed

view run

@ahmedGaber93

Copy link
Copy Markdown
Contributor

All yours @puneetlath @tgolen

@puneetlath
puneetlath merged commit c408634 into main Sep 3, 2026
43 of 44 checks passed
@puneetlath
puneetlath deleted the claude-refactorInlineEditConnectWithoutView branch September 3, 2026 14:45
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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

@OSBotify

OSBotify commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/puneetlath in version: 9.4.70-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 No help site changes are required for this PR. I did not create a docs PR.

Why: this is a pure internal refactor. It moves Onyx reads out of TransactionInlineEdit.ts and into the calling hook so the edit actions become pure functions. Nothing user-facing changes — no new feature, no new flow, no renamed tab, button, or setting.

What I checked

Files changed — all internal source and tests, none under docs/:

Existing help site coverage of inline editing — I read the three articles that document it, and each still describes the behavior correctly after this PR:

The editable fields the PR's test plan lists (Amount, Date, Merchant, Description, Category, Tag) and the entry points (SpendExpenses, and expense reports from Inbox) are unchanged, so the articles need no edits.

@ahmedGaber93, there is no help site PR to link — this refactor doesn't change anything documented on the help site. If you believe a user-facing behavior did change here, reply with what changed and I'll draft the docs PR.


view run

@OSBotify

OSBotify commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/puneetlath in version: 9.4.72-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No help site changes are required for this PR. I did not create a docs PR.

Why: This is a pure refactor with no user-facing change. It removes seven module-level Onyx.connectWithoutView() subscriptions from src/libs/actions/TransactionInlineEdit.ts and turns the edit actions into pure functions that take their data as parameters, with src/hooks/useTransactionInlineEdit.ts reading those values via useOnyx and passing them in.

What I checked

All five changed files are source or test files:

  • src/hooks/useDuplicateTransactionsAndViolations.ts — adds useLiveDuplicateTransactionsAndViolations
  • src/hooks/useTransactionInlineEdit.ts — reads Onyx values and passes them to the actions
  • src/libs/actions/TransactionInlineEdit.ts — subscriptions removed, params added
  • tests/actions/IOUTest/UpdateMoneyRequestTest.ts — updated for the new params
  • tests/unit/inlineEditing/TransactionInlineEdit.test.ts — updated for the new params

Nothing here changes a feature name, a settings label, a button, a menu path, or a user-visible flow. The PR body states it directly: "Behavior is unchanged — this is a pure refactor." The test plan only asks a reviewer to confirm existing inline editing still works, which means no documented behavior moved.

Help site articles under docs/articles describe what users see and do. Since the inline editing behavior for Amount, Date, Merchant, Description, Category, and Tag is identical before and after, there is nothing to add, remove, or correct.

There is no linked help site PR to review. If you think a specific article is now out of date, tell me which one and I'll create the draft PR.

@ahmedGaber93, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review


view run

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

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants