Skip to content

feat(sdk): add sorting + RQL infinite loading to views-new invoices#1553

Open
paanSinghCoder wants to merge 11 commits intomainfrom
feat/add-sorting-and-pagination-invoices
Open

feat(sdk): add sorting + RQL infinite loading to views-new invoices#1553
paanSinghCoder wants to merge 11 commits intomainfrom
feat/add-sorting-and-pagination-invoices

Conversation

@paanSinghCoder
Copy link
Copy Markdown
Contributor

@paanSinghCoder paanSinghCoder commented Apr 20, 2026

Summary

  • Swap views-new billing `Invoices` component from the plain `listInvoices` RPC to `FrontierServiceQueries.searchOrganizationInvoices` with `useInfiniteQuery` (server-mode pagination).
  • Enable server-side sorting on Date, Status, and Amount columns via `<DataTable.Toolbar />` + DisplaySettings.
  • Default sort: `createdAt desc` (matching the admin invoices view).
  • `listInvoices` call remains in `billing-view.tsx` to feed `PaymentIssue` (which needs `dueDate`/`hostedUrl` not present in the RQL response shape).
  • Duplicate `connect-pagination.ts` and `transform-query.ts` into `sdk/react/utils/` (kept independent from `sdk/admin/utils/` — the admin and react entrypoints deploy separately, so utilities can't be shared via direct imports).
Screen.Recording.2026-04-20.at.1.14.52.PM.mov

Field mapping

The RQL response (`OrganizationInvoice`) has a trimmed projection vs the full `Invoice`:

  • `effectiveAt` (with draft → `dueDate` fallback) → `createdAt`
  • `hostedUrl` → `invoiceLink`
    Other fields (`state`, `amount`, `currency`) map 1:1.

Depends on

Test plan

  • `pnpm run build` (web turbo) passes
  • `docker-compose build` succeeds against the local proton tarball
  • Manual: change sort via DisplaySettings; Reset to default snaps back to createdAt desc
  • Manual: scroll triggers `fetchNextPage` (infinite loading)
  • Manual: PaymentIssue banner still fires for past-due subscriptions (uses the untouched `listInvoices` call)

🤖 Generated with Claude Code

paanSinghCoder and others added 2 commits April 17, 2026 16:42
Move the RPC from AdminService to FrontierService so org admins (not only
platform superusers) can list their own org's invoices. Matches the gate
pattern already used by FrontierService/ListInvoices (UpdatePermission on
the org namespace). Superusers still pass via the standard interceptor
bypass.

- Bump PROTON_COMMIT to pick up the proto move (raystack/proton#476).
- Regenerate proto/v1beta1 via `make proto`.
- Swap authorization.go entry from IsSuperUser to IsAuthorized(org, UpdatePermission).
- Switch the admin dashboard frontend from AdminServiceQueries to
  FrontierServiceQueries; request/response shape is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
frontier Error Error Apr 21, 2026 10:40am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 20, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Invoice list now uses server-driven infinite pagination with load-more behavior.
    • Invoice rows now link directly to invoice pages.
  • Enhancements

    • Status column adds grouping indicators, sorting, and hide/show controls.
    • Additional invoice statuses supported (draft, void, uncollectible).
  • Bug Fixes / UX

    • Improved error handling and a dedicated error empty state when invoice retrieval fails.

Walkthrough

Updates Proton commit in Makefile, adds RQL React utilities for pagination and query transformation, extends invoice state constants, and refactors the Invoices component to internally perform server-backed infinite loading via useInfiniteQuery (BillingView stops passing invoices/isLoading).

Changes

Cohort / File(s) Summary
Build Configuration
Makefile
Updated PROTON_COMMIT hash used for downloading Proton archive (changes protobuf generation input).
React Utilities
web/sdk/react/utils/connect-pagination.ts, web/sdk/react/utils/transform-query.ts
Added pagination helpers (DEFAULT_PAGE_SIZE, getConnectNextPageParam, getGroupCountMapFromFirstPage) and transformDataTableQueryToRQLRequest plus TransformOptions for mapping DataTable queries to RQL requests.
Constants
web/sdk/react/utils/constants.ts
Extended INVOICE_STATES to include DRAFT, VOID, and UNCOLLECTIBLE.
Billing Components
web/sdk/react/views-new/billing/billing-view.tsx, web/sdk/react/views-new/billing/components/invoices.tsx
BillingView stops supplying invoices/isLoading; Invoices converted to internal server-backed infinite-query flow, owns DataTableQuery state, debounces and transforms queries, handles pagination/error empty states, and changed column accessors/types.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • rohanchkrabrty
  • rohilsurana
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/add-sorting-and-pagination-invoices

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 20, 2026

Coverage Report for CI Build 24657517656

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage remained the same at 42.257%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 36962
Covered Lines: 15619
Line Coverage: 42.26%
Coverage Strength: 11.85 hits per line

💛 - Coveralls

Base automatically changed from refactor/search-org-invoices-to-frontier-service to main April 20, 2026 08:53
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 822a8b82-d707-4637-b968-776c4e14c15c

📥 Commits

Reviewing files that changed from the base of the PR and between cd1a62d and 1b8a353.

📒 Files selected for processing (6)
  • Makefile
  • web/sdk/react/utils/connect-pagination.ts
  • web/sdk/react/utils/constants.ts
  • web/sdk/react/utils/transform-query.ts
  • web/sdk/react/views-new/billing/billing-view.tsx
  • web/sdk/react/views-new/billing/components/invoices.tsx

Comment on lines +31 to +34
// Use pagination info from response to determine next page
const pagination = lastPage.pagination;
const currentOffset = pagination?.offset || 0;
const limit = pagination?.limit || DEFAULT_PAGE_SIZE;
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.

⚠️ Potential issue | 🟡 Minor

Preserve valid pagination values when falling back.

offset and limit are numeric fields, so || treats valid 0 values as missing. Prefer nullish fallback and use the request query as the next fallback before DEFAULT_PAGE_SIZE.

Proposed fix
-  const currentOffset = pagination?.offset || 0;
-  const limit = pagination?.limit || DEFAULT_PAGE_SIZE;
+  const currentOffset = pagination?.offset ?? queryParams.query.offset ?? 0;
+  const limit = pagination?.limit ?? queryParams.query.limit ?? DEFAULT_PAGE_SIZE;

Comment on lines +36 to +41
const DEFAULT_SORT: DataTableSort = { name: 'createdAt', order: 'desc' };

const INITIAL_QUERY: DataTableQuery = {
offset: 0,
limit: DEFAULT_PAGE_SIZE
};
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n web/sdk/react/views-new/billing/components/invoices.tsx | head -100

Repository: raystack/frontier

Length of output: 3545


🏁 Script executed:

cat -n web/sdk/react/views-new/billing/components/invoices.tsx | sed -n '100,250p'

Repository: raystack/frontier

Length of output: 4321


🏁 Script executed:

cat -n web/sdk/react/utils/transform-query.ts

Repository: raystack/frontier

Length of output: 4223


🏁 Script executed:

rg -l "DataTable" --type tsx --type ts web/sdk/react/ | head -10

Repository: raystack/frontier

Length of output: 88


🏁 Script executed:

rg -l "DataTable" web/sdk/react/ | grep -E "\.(tsx?|jsx?)$" | head -10

Repository: raystack/frontier

Length of output: 565


🏁 Script executed:

rg -A 20 "const INITIAL_QUERY" web/sdk/react/ | head -80

Repository: raystack/frontier

Length of output: 1781


🏁 Script executed:

rg -B 5 -A 15 "defaultSort" web/sdk/react/ | head -100

Repository: raystack/frontier

Length of output: 8153


🏁 Script executed:

rg -B 10 'mode="server"' web/sdk/react/ | grep -A 10 "INITIAL_QUERY\|defaultSort"

Repository: raystack/frontier

Length of output: 225


🏁 Script executed:

rg -B 5 -A 10 "onTableQueryChange.*mount\|useEffect.*onTableQueryChange\|useEffect.*defaultSort" web/sdk/react/

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

rg -B 5 -A 10 "useEffect" web/sdk/react/views-new/billing/components/invoices.tsx

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

rg "useEffect\|useMemo" web/sdk/react/views-new/billing/components/invoices.tsx -A 3

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

cat web/sdk/react/views-new/billing/components/invoices.tsx | grep -A 3 "useEffect\|useMemo"

Repository: raystack/frontier

Length of output: 545


🏁 Script executed:

cd web && find . -name "package.json" -o -name "package-lock.json" -o -name "yarn.lock" | head -5

Repository: raystack/frontier

Length of output: 177


🏁 Script executed:

cat web/sdk/react/package.json | grep -A 5 -B 5 "apsara"

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

find web/sdk/react -name "*.json" -exec grep -l "apsara" {} \;

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

find web/sdk/react -type f -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "defaultSort\|onTableQueryChange" 2>/dev/null | head -5

Repository: raystack/frontier

Length of output: 43


Seed the server query with the default sort.

defaultSort configures the table UI, but the initial RQL request is built from INITIAL_QUERY, which currently has no sort. The transformDataTableQueryToRQLRequest function does not apply a default sort—it only transforms what exists in query.sort. This means the first server page loads without any sort order, requiring a user interaction (column click) to trigger onTableQueryChange and add sorting. Add the default sort to INITIAL_QUERY so the first server request is deterministically createdAt desc.

Proposed fix
 const INITIAL_QUERY: DataTableQuery = {
   offset: 0,
-  limit: DEFAULT_PAGE_SIZE
+  limit: DEFAULT_PAGE_SIZE,
+  sort: [DEFAULT_SORT]
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const DEFAULT_SORT: DataTableSort = { name: 'createdAt', order: 'desc' };
const INITIAL_QUERY: DataTableQuery = {
offset: 0,
limit: DEFAULT_PAGE_SIZE
};
const DEFAULT_SORT: DataTableSort = { name: 'createdAt', order: 'desc' };
const INITIAL_QUERY: DataTableQuery = {
offset: 0,
limit: DEFAULT_PAGE_SIZE,
sort: [DEFAULT_SORT]
};

Comment on lines +173 to +180
} = useInfiniteQuery(
FrontierServiceQueries.searchOrganizationInvoices,
{ id: organizationId, query },
{
enabled: !!organizationId,
pageParamKey: 'query',
getNextPageParam: lastPage =>
getConnectNextPageParam(lastPage, { query }, 'organizationInvoices'),
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the backend implementation of SearchOrganizationInvoices
fd -t f 'service\.go$' | grep -i invoice | head -20

Repository: raystack/frontier

Length of output: 311


🏁 Script executed:

# Also search for the RQL query construction and any filtering logic
rg 'SearchOrganizationInvoices|searchOrganizationInvoices' -t go -A 5 -B 2

Repository: raystack/frontier

Length of output: 37957


🏁 Script executed:

# Find the billing invoice service implementation
fd -t f -p 'billing/invoice' | grep -E '\.go$'

Repository: raystack/frontier

Length of output: 146


🏁 Script executed:

# Look for nonzeroAmountOnly or zero-amount filtering logic in the codebase
rg 'nonzeroAmountOnly|zero.*amount|zeroAmount' -t go -B 2 -A 2

Repository: raystack/frontier

Length of output: 6896


🏁 Script executed:

# Read the SearchOrganizationInvoices implementation
sed -n '/func (h \*ConnectHandler) SearchOrganizationInvoices/,/^func /p' internal/api/v1beta1connect/organization_invoices.go | head -80

Repository: raystack/frontier

Length of output: 2330


🏁 Script executed:

# Also check the orginvoices service to see if there's default filtering
fd -t f 'service.go' -path '*/orginvoices/*' | head -5

Repository: raystack/frontier

Length of output: 229


🏁 Script executed:

# Look at the old billing-view.tsx to see how the old query was constructed
fd -t f 'billing-view.tsx' | head -3

Repository: raystack/frontier

Length of output: 109


🏁 Script executed:

# Find and read the orginvoices service
fd -t f -p 'core/aggregates/orginvoices' '\.go$'

Repository: raystack/frontier

Length of output: 153


🏁 Script executed:

# Read the orginvoices service
cat core/aggregates/orginvoices/service.go | head -150

Repository: raystack/frontier

Length of output: 1437


🏁 Script executed:

# Check the old billing-view.tsx
cat web/sdk/react/views-new/billing/billing-view.tsx | head -300

Repository: raystack/frontier

Length of output: 4483


🏁 Script executed:

# Read invoices.tsx around lines 173-180 to see the full context and how query is initialized
cat -n web/sdk/react/views-new/billing/components/invoices.tsx | sed -n '1,200p'

Repository: raystack/frontier

Length of output: 6762


Add an RQL filter to exclude zero-amount invoices from the search query.

The old listInvoices path used nonzeroAmountOnly: true, but SearchOrganizationInvoices has no equivalent parameter and the backend applies no default filtering. The initial RQL query passed to the search request (lines 159–164) contains only pagination parameters, so zero-amount invoices will be displayed.

Either add an amount filter clause to the initial query or implement a nonzero_amount_only parameter in the SearchOrganizationInvoicesRequest proto and backend handler.

: (getValue() as TimeStamp);
const timestamp = value || row?.original?.createdAt;
const date = timestampToDayjs(timestamp);
accessorKey: 'createdAt',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lets use created_at here

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.

Done

limit: DEFAULT_PAGE_SIZE
};

const TRANSFORM_OPTIONS = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we can remove this

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.

Done

isLoading: boolean;
invoices: Invoice[];
}
const DEFAULT_SORT: DataTableSort = { name: 'createdAt', order: 'desc' };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lets use created_at here

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.

Done.

<DataTable.Content
emptyState={noDataChildren}
/>
<Flex direction="column" style={{ width: '100%' }}>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

use width props

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.

We have a task to remove width prop from Flex. Let's keep inline style here till we make a decision on it.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
web/sdk/react/views-new/billing/components/invoices.tsx (2)

38-41: ⚠️ Potential issue | 🟠 Major

Seed INITIAL_QUERY with DEFAULT_SORT.

defaultSort only configures the UI; the initial RQL request built from INITIAL_QUERY has no sort, so the first page loads unsorted until the user interacts with the table. Including sort: [DEFAULT_SORT] here ensures the first request is deterministically created_at desc.

Proposed fix
 const INITIAL_QUERY: DataTableQuery = {
   offset: 0,
-  limit: DEFAULT_PAGE_SIZE
+  limit: DEFAULT_PAGE_SIZE,
+  sort: [DEFAULT_SORT]
 };

168-181: ⚠️ Potential issue | 🟠 Major

Zero-amount invoices are no longer filtered out.

The previous listInvoices call used nonzeroAmountOnly: true, but SearchOrganizationInvoices has no equivalent parameter and this RQL query does not include an amount filter. Either add a filter clause for amount != 0 to the initial query or extend the proto/handler to support nonzero_amount_only.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a4d5199-4d41-4911-bae1-2ad0b06e2f24

📥 Commits

Reviewing files that changed from the base of the PR and between 1b8a353 and afbedcf.

📒 Files selected for processing (1)
  • web/sdk/react/views-new/billing/components/invoices.tsx

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.

3 participants