Repository navigation
fix(frontend): type getUpcomingPurchases as the { purchases } envelope - #1397
Conversation
getUpcomingPurchases() declared Promise<UpcomingPurchase[]>, but
GET /api/dashboard/upcoming returns { purchases: [...] }, so both
dashboard call sites cast the result to an ad-hoc object type and tsc
never checked them. The function now returns UpcomingPurchasesResponse,
whose purchases field is UpcomingPurchase[] | null because the Go
handler serializes an empty result from a nil slice as null.
The casts are gone. The dashboard's hand-kept UpcomingPurchase copy in
types.ts (provider narrowed to Provider) is now an alias of the API
type, since that narrowing was what forced a cast; the widget already
treats provider as an arbitrary string. Reverting the return type to an
array now fails typecheck at both call sites and in the new api test.
Refs LeanerCloud#1189
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 7 billable files and costs up to $1.75. Or wait 59 minutes for your next included review. View limit details
Comment |
Conflicts:
- frontend/src/__tests__/api.test.ts: import block; kept main's new
base64Encode import from ../api/client and added this branch's
UpcomingPurchasesResponse to the shared type import.
dashboard.ts merged cleanly: main's rework keeps both
api.getUpcomingPurchases() call sites, which still read the typed
{ purchases } envelope without casts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Local verification at
|
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at |
No textual conflicts. Checked main's LeanerCloud#1397 (getUpcomingPurchases now typed as the { purchases } envelope; dashboard UpcomingPurchase is an alias of the API type): lib/upcoming-scope.ts and renderUpcomingPurchases still typecheck against it, and the provider scope applies to data?.purchases on both the initial load and the Retry path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/app.ts: import block only. This branch imports setOpenAccountsHandler (first-run card's Admin > Accounts navigation); main's LeanerCloud#1336 imports initPurchasingNav. Kept both imports; both call sites merged cleanly and are unchanged. Also checked: main's LeanerCloud#1397 envelope typing for getUpcomingPurchases sits next to this branch's 4th allSettled entry (listAccountsMinimal) in loadDashboard and merged cleanly. LeanerCloud#1368's "Select none" rename is the per-column popover button, separate from this branch's "Clear filters" (clearAllColumnFilters) action. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
getUpcomingPurchases()declaredPromise<UpcomingPurchase[]>, butGET /api/dashboard/upcomingreturns{ purchases: [...] }, and both dashboard call sites cast around the mismatch, sotscnever checked how the response was used. There is no user-visible bug today; this PR types the real envelope and removes the casts so a future refactor can't silently break the Home upcoming-purchases widget.Closes #1189
What changed
frontend/src/api/types.ts: newUpcomingPurchasesResponse { purchases: UpcomingPurchase[] | null }(nullable because the Go handler serializes an empty nil slice asnull).frontend/src/api/dashboard.ts:getUpcomingPurchases(): Promise<UpcomingPurchasesResponse>; the type is exported fromfrontend/src/api/index.ts.frontend/src/dashboard.ts: both casts removed (loadDashboardandretryUpcomingPurchases).frontend/src/types.ts: the duplicated localUpcomingPurchaseinterface (withprovidernarrowed toProvider) is nowexport type UpcomingPurchase = api.UpcomingPurchase, since that narrowing was what forced a cast.api.test.ts"resolves the { purchases } envelope" (with a typed annotation, so typecheck also guards it); a parametrizeddashboard.test.tscase forpurchases: nulland a null response, both showing the empty state and no error.Commits:
cdc5bf35, and the merge with maine7956be9.Why this approach
It matches the existing
PlannedPurchasesResponsepattern. Typingpurchasesas nullable reflects the real wire format and makestscrequire the existing|| []guard. Aliasing the local type instead of keeping two hand-synced copies removes the drift that caused the cast; the widget only usesprovideras a string (badge class whitelist, uppercase label), so widening it loses nothing. Temporarily reverting the return type to an array produced 4tscerrors (both dashboard call sites, the new api test and the then-unused import), so the type change is load-bearing.Screenshots
No visible change: this is a types-only change; the dashboard renders the same.
Verification
Run on this branch's head
e7956be9(level with main51aa5b13), fromfrontend/:npm run lint: 0 errors. Typecheck and build pass.e2e-local.sh): 208 passed.scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.frontend/src/__tests__/api.test.tsresolved, see the merge commit.Notes for review
Aliasing the local
types.tsUpcomingPurchasetoapi.UpcomingPurchasewidensproviderfromProvidertostringfor the dashboard. Keeping a separate view type plus a mapping function is the alternative if the narrower type is preferred.Overlaps
#713 changes
renderUpcomingPurchasesin the same file (frontend/src/dashboard.ts).git merge-treereports a clean merge and the types stay compatible, but both touch the same widget, so they are best reviewed and landed together.Note for the UI owner
This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code