Repository navigation
Fix import/no-cycle - part 15 - #101721
roryabraham merged 25 commits into
Conversation
canIOUBePaid, canApproveIOU, getBadgeFromIOUReport and getIOUReportActionWithBadge are pure read-side predicates, but they lived in libs/actions/IOU/ReportWorkflow. Every component that needed them had to import an actions module, which is the edge keeping the ReportUtils <-> IOU actions cycle alive. Move them into ReportUtils and update the import sites. ReportWorkflow keeps only the write-side actions. ReportUtils gains a module-scoped TRANSACTION_VIOLATIONS cache for the SUBMIT badge, which is computed inside the reportAttributes derived value; the deprecated getAllTransactionViolations() getter in IOU/index.ts is not importable from here without recreating the cycle.
Drop the new Onyx.connectWithoutView violations cache in ReportUtils and pass the collection from the reportAttributes derived value instead. Thread it through getReasonAndReportActionThatRequiresAttention into getIOUReportActionWithBadge/getBadgeFromIOUReport down to canSubmitAndIsAwaitingForCurrentUser. generateReportAttributes already had the collection in scope, so no new module-level Onyx state is needed.
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
Nothing outside ReportUtils imports it now that the workflow predicates live here.
e11875e to
ec3474b
Compare
isPaidGroupPolicy and isPaidGroupPolicyByType are the identical TEAM/CORPORATE check, but only isPaidGroupPolicy is a restricted import name. ReportUtils already uses the type-based sibling isGroupPolicyByType(policy?.type), so switch canApproveIOU/canIOUBePaid to isPaidGroupPolicyByType(policy.type) and drop the no-restricted-imports disable. No behavior change.
ReportWorkflow keeps the deletion of the moved predicates, and upstream's getReimbursementChoice reads are ported into canIOUBePaid in ReportUtils. Dropped the now-unused getReimbursementChoice import in ReportWorkflow.
The only callers moved to ReportUtils with canIOUBePaid.
|
🤖 Code review — The refactor is sound: One real regression, and it's the one you flagged yourself. Dropping the deprecated global violations read from 1.
|
| Call site | Policy source | Matches old lookup |
|---|---|---|
MoneyReportHeaderEducationalModals.tsx |
POLICY[moneyRequestReport.policyID] |
✅ |
RemoveHoldPrimaryAction.tsx:34 |
POLICY[moneyRequestReport.policyID] |
✅ |
useHoldRejectActions.ts |
POLICY[moneyRequestReport.policyID] |
✅ |
BaseReportActionContextMenu.tsx:239 |
POLICY[REPORT[moneyRequestAction.reportID].policyID] |
✅ |
MoneyRequestHeaderPrimaryAction.tsx:71 |
POLICY[transactionThread.policyID] |
✅ — buildTransactionThread copies policyID from the money request report (ReportUtils.ts:9843) |
MoneyRequestHeaderSecondaryActions.tsx:151 |
same as above | ✅ |
The BaseReportActionContextMenuTest change is a good trade: it stops asserting unholdRequest args but now pins the new policy argument, and HoldTest's two new cases cover the hold/unhold branches directly. Net coverage is up.
5. Nits
- Stale aliases.
canIOUBePaid as canIOUBePaidActionandcanIOUBePaidIOUActions(useReportPreviewActionDecision.ts:8,useLifecycleActions.tsx:13) now name aReportUtilsread helper "Action". Dropping the alias in this PR avoids another rename pass later. - Mixed module alias. This PR adds both
@libs/actions/IOU/Holdand@userActions/IOU/Holdfor the same module. Pick one. - Lost TODO. The
// TODO: https://github.com/Expensify/App/issues/66512that documented the deprecated global read is gone, butReportUtils.ts:3371still suppressesno-deprecatedforgetReportTransactions. Re-add the link so that call stays tracked. - Import member order.
canIOUBePaidlands beforecanEditMultipleTransactionsinuseSearchBulkActions.tsandhasAnyTransactionWithoutRTERViolationafterhasNoticeTypeViolationinReportUtils.ts. ESLint doesn't enforce it, so cosmetic only. - Removing
isPayAtEndExpenseReportfrom the export list is correct — the only three references are insideReportUtilsitself.
Reply with @MelvinBot <feedback> to refine this review.
ReportWorkflow keeps the deletion of the moved predicates. Upstream's held-expense exclusion and the report-actions parameter for getBadgeFromIOUReport land in the moved copies in ReportUtils, where the collection param is named allReportActionsParam to avoid shadowing the module-level cache.
…eportutils-workflow-reads-v1
Its only outside caller was the canIOUBePaid/getBadgeFromIOUReport pair that now lives here, so the export is dead. Knip flagged it as a new unused export on this branch.
…eportutils-workflow-reads-v1
|
@MelvinBot review |
|
🤖 Re-review — Every finding from my last pass is fixed, and I found nothing new that blocks merge. All checks green except Prior findings — all resolved ✅1.
I verified the positional-argument threading at all four call sites — the new param lands in the right slot every time. 2. Missing regression test — fixed, twice. 3–5. Nits — all addressed. The What I re-checked on the deltaVerification details
One note, not a blocker
Also cosmetic: This review is static analysis only — the browser environment was unavailable for this run, so I could not exercise Tests 1–4 in NewDot. Your Test 2 (auto-rejected suppression) is now covered by unit tests; Tests 1, 3, and 4 still want a manual pass. Reply with |
…list and debug LHN reasons
…ycle-part15-reportutils-workflow-reads-v1
|
@thelullabyy 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] |
thelullabyy
left a comment
There was a problem hiding this comment.
[lullabyy agent] Reviewed the move of the read-only workflow checks into ReportUtils and of changeMoneyRequestHoldStatus into Hold.ts. The moved logic matches the original, and every hold caller passes the expense report's policy, the same one the old global lookup returned. I left a few inline comments: one about the API, one about lost test coverage, and one about test docs.
| currentUserLogin: string, | ||
| currentUserAccountID: number, | ||
| iouReportActions: OnyxEntry<ReportActions>, | ||
| allViolations?: OnyxCollection<TransactionViolations>, |
There was a problem hiding this comment.
[lullabyy agent] allViolations is optional here and in getIOUReportActionWithBadge (line 3443). The old code always read the global violations cache. If a caller leaves this argument out, canSubmitAndIsAwaitingForCurrentUser gets undefined, the "every expense auto-rejected" check silently stops working, and the SUBMIT badge comes back with no type error. This PR is meant to fix exactly that case, so could we make the parameter required (allViolations: OnyxCollection<TransactionViolations> | undefined)? Then every current and future caller has to choose what to pass. The same applies to the new transactionViolations? parameter on getReasonAndReportActionThatRequiresAttention and requiresAttentionFromCurrentUser.
| mockedCanSubmitReport.mockReturnValue(true); | ||
| const result = canSubmitAndIsAwaitingForCurrentUser(iouReport, chatReport, basePolicy, transactions, {}, 'user@test.com', CURRENT_USER_ACCOUNT_ID, undefined); | ||
| expect(result).toBe(true); | ||
| }); |
There was a problem hiding this comment.
[lullabyy agent] This PR removes the forwards the passed iouReportOwnerLogin instead of looking it up in the personal details list test because canSubmitReport now lives in the same module and can't be mocked. That leaves the iouReportOwnerLogin ?? getLoginByAccountID(...) branch in canSubmitAndIsAwaitingForCurrentUser with no tests. Could we add a behavior-based test instead? For example, seed no personal details, add a transaction with a pending RTER violation that belongs to 'threaded.owner@test.com', and assert that the result changes depending on whether the owner login is passed.
There was a problem hiding this comment.
canSubmitAndIsAwaitingForCurrentUser passes undefined violations to canSubmitReport, and main does the same. The owner login only matters when a dismissed violation gets checked, so on this path the result is the same with or without the fallback, and a behavior test can’t see it. I added a test on canSubmitReport instead, since that’s where the login decides whether a dismissal counts. Passing the violations through would change the SUBMIT badge, so I don't want to touch it in this PR
| expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.SUBMIT); | ||
| }); | ||
|
|
||
| it('should not return SUBMIT actionBadge when every transaction of the report was auto-rejected', async () => { |
There was a problem hiding this comment.
[lullabyy agent] This new test has // Then comments but no // Given or // When comments. Per tests/README.md#documenting-tests, every test case should explain why it does what it does. Please add a // Given comment above the setup (an open report whose only expense is auto-rejected, with the current user as owner and manager) and a // When comment above the getIOUReportActionWithBadge call. The first two expects also need a // Then comment.
| }); | ||
|
|
||
| it('calls unholdRequest when pressing the unhold action', async () => { | ||
| it('calls changeMoneyRequestHoldStatus with the money request policy when pressing the unhold action', async () => { |
There was a problem hiding this comment.
[lullabyy agent] This test was renamed and rewritten to check the new changeMoneyRequestHoldStatus contract, so it would be a good time to add the required // Given / // When / // Then comments. For example: Given an on-hold expense whose report belongs to policyID; When the Unhold menu item is pressed; Then the hold action gets the money request report's policy rather than the chat's.
|
@LukasMod Overall PR LGTM, I left some comments from my agent. Please check and resolve conflicts as well so that I can approve the PR |
…eportutils-workflow-reads-v1 Keep the workflow predicates (canApproveIOU, canIOUBePaid, canSubmitReport, getBadgeFromIOUReport) in ReportUtils, where this branch moved them, and port upstream's canAdminPayReport() helper into canIOUBePaid.
Optional violations parameters let a caller silently drop the auto-rejected-expense check and resurrect the SUBMIT badge with no type error. Make allViolations/transactionViolations required (OnyxCollection | undefined) on getBadgeFromIOUReport, getIOUReportActionWithBadge, getReasonAndReportActionThatRequiresAttention and requiresAttentionFromCurrentUser, hoisting each to the last required position since TS forbids a required parameter after an optional one.
|
@thelullabyy thanks for review, I applied feedback and answered 👍 conflicts resolved |
|
@LukasMod Thank you for the update. Changes look good, however, it got conflicts again :(( |
…eportutils-workflow-reads-v1 # Conflicts: # src/libs/ReportUtils.ts # src/libs/actions/IOU/ReportWorkflow.ts
|
@thelullabyy resolved! :) |
…eportutils-workflow-reads-v1 # Conflicts: # tests/unit/ReportUtilsTest.ts
|
I updated getReasonAndReportActionThatRequiresAttention to pass eslint max params issue after last merge |
…eportutils-workflow-reads-v1 # Conflicts: # src/libs/ReportUtils.ts # tests/ui/components/BaseReportActionContextMenuTest.tsx # tests/unit/ReportUtilsTest.ts
…eportutils-workflow-reads-v1 # Conflicts: # src/libs/actions/IOU/ReportWorkflow.ts
|
Conflicts each 2 hours 😭 |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚧 roryabraham has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/roryabraham in version: 9.5.6-0 🚀
|
|
No help site update needed. This PR moves IOU workflow and hold logic between modules to clear import cycles, so no existing article sentence becomes incorrect. |
|
🚀 Deployed to production by https://github.com/puneetlath in version: 9.5.6-6 🚀
Bundle Size Analysis (Sentry): |
Explanation of Change
canApproveIOU,canIOUBePaid,canSubmitReport,getBadgeFromIOUReport,getIOUReportActionWithBadge) moved fromactions/IOU/ReportWorkflow.tsintoReportUtils.ts.ReportUtilsno longer importsReportWorkflow, which clears the import loop.ReportWorkflowkeeps only the write actions (submit, approve, pay, reopen, retract).changeMoneyRequestHoldStatusmoved fromReportUtilsintoactions/IOU/Hold.ts, soReportUtilsno longer importsunholdRequest. It now takespolicyfrom the caller instead of looking it up in a global cache.useOptimisticNextStep. The "every expense auto-rejected" rule that hides the Submit badge depends on this.canIOUBePaidnow reads the chat's archived flag fromReportUtils' own copy of the report name-value pairs. It is the same Onyx collection, read through a different module.requiresAttentionfrom the report attributes instead of recomputing it.isPayAtEndExpenseReportandisReportExcludedForHeldExpensesare no longer exported, andisPaidGroupPolicy(policy)becameisPaidGroupPolicyByType(policy.type), which has the same body.Covered by automated tests
tests/actions/IOUTest/ReportWorkflowTest.ts:5100.tests/unit/DebugUtilsTest.ts:1154and:1385.changeMoneyRequestHoldStatusnavigates to the hold reason or unholds:tests/actions/IOUTest/HoldTest.ts:513and:526.tests/ui/components/BaseReportActionContextMenuTest.tsx:408.ReportWorkflowTestcases, now importing fromReportUtils.import/no-cycle cleared (whole repo, 120 → 99)
Fixed Issues
$ #99650
PROPOSAL:
Tests
Prerequisites: a team workspace with approvals on; account A approves and pays, account S submits.
Test 1: Smoke test for workflow buttons, badges and hold
Test 2: Auto-rejected expenses hide the Submit badge in the LHN
Prerequisite: a report where every one of S's expenses was auto-rejected and A is the report manager.
Test 3: No Pay option in an archived chat
Archiving is automatic, there is no manual "Archive" button. A chat is archived
when its workspace/account relationship ends (member removed, workspace deleted,
account closed/merged). Archived chats appear under the "Archived" header in the LHN.
Prerequisite: you are the invoice receiver/payer of an unpaid invoice; the invoice chat then gets
archived (e.g. the sending workspace is deleted).
Test 4: Unread count still includes reports that need attention
Prerequisite: account A has an unread chat whose report is waiting on A to Approve or Pay.
Offline tests
QA Steps
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
android.native.mov
Android: mWeb Chrome
android.web.mov
iOS: Native
ios.native.1-2.mov
ios.native.3.mov.mov
ios.native.4.mov
iOS: mWeb Safari
ios.web.1-4.mov
MacOS: Chrome / Safari
web-1.mov
web-2.mov
web.4.mov