Skip to content

feat: export Organisations page as a component and consume it in apps/admin#1397

Open
paanSinghCoder wants to merge 7 commits intofeat/export-users-pagefrom
feat/export-organisations-page
Open

feat: export Organisations page as a component and consume it in apps/admin#1397
paanSinghCoder wants to merge 7 commits intofeat/export-users-pagefrom
feat/export-organisations-page

Conversation

@paanSinghCoder
Copy link
Contributor

Migrate Organizations from app to lib

Summary

Moves the Organizations list and details views from web/apps/admin into web/lib/admin, following the same pattern as the Users page. The app keeps thin wrapper pages that wire config, navigation, and export callbacks; the lib holds the UI and data logic.

Changes

Lib (web/lib/admin)

  • Move

    • apps/admin/src/pages/organizations/*lib/admin/views/organizations/ (list, details with layout, contexts, security, members, projects, invoices, tokens, apis, side-panel, edit).
  • Organization context

    • Added appUrl, tokenProductId, countries, organizationTypes so details/edit/add panels get config from the app via context instead of AppContext.
  • Details

    • OrganizationDetails no longer takes children; renders <Outlet context={{ organization }} /> so child routes (e.g. Security) can use useOutletContext<OutletContext>().
    • OrganizationDetailsProps: organizationId from caller; optional onExportMembers, onExportProjects, onExportTokens; optional appUrl, tokenProductId, countries, organizationTypes.
  • List

    • OrganizationList takes optional appName, onNavigateToOrg, onExportCsv, organizationTypes, appUrl, countries; create panel and row navigation are callback-driven.
  • Removed app-only usage

    • Replaced AppContext / config with context props or OrganizationContext (e.g. org-details-section, add-tokens-dialog, edit/organization).
    • Replaced ~/ imports with lib-relative paths; use lib PageTitle, connect-timestamp, constants, helper, AssignRole, UsersIcon (and MixIcon for token icon).
    • Countries and organization types come from the app and are passed in (no loadCountries in lib).
  • Exports (lib/admin/index.ts)

    • Exported: OrganizationList, OrganizationDetails, OrganizationSecurity, OrganizationMembersPage, OrganizationProjectssPage, OrganizationInvoicesPage, OrganizationTokensPage, OrganizationApisPage (and related types where used).
  • Build

    • Admin tsup: target: 'es2020' (BigInt), usehooks-ts in externals; usehooks-ts added as devDependency for DTS. Fixed relative imports under views/organizations for correct resolution. Layout: single Flex import.

App (web/apps/admin)

  • New wrappers

    • pages/organizations/list/index.tsx: Loads countries, reads AppContext.config; passes onNavigateToOrg, onExportCsv (via adminClient.exportOrganizations), organizationTypes, appUrl, countries to lib OrganizationList.
    • pages/organizations/details/index.tsx: Loads countries; passes organizationId from useParams(), appUrl, tokenProductId, countries, organizationTypes, and onExportMembers / onExportProjects / onExportTokens (via admin client export methods) to lib OrganizationDetails.
  • Routes

    • Organizations routes now use OrganizationListPage and OrganizationDetailsPage; nested detail routes import org sub-views from @raystack/frontier/admin.

Bug fix

  • Edit organization panel: Replaced leftover config?.app_url (undefined after removing AppContext) with appUrl from OrganizationContext, fixing "config is not defined" when opening Members → Edit → Organisation.

Testing

  • Organizations list loads and table/search work.
  • Opening an organization shows details layout and tabs (Members, Security, Projects, Invoices, Tokens, APIs).
  • Members → Edit → Organisation opens edit panel; Organisation URL prefix shows correctly (no "config is not defined").
  • Create organization and export (list CSV, member/project/token exports) work when configured.

@vercel
Copy link

vercel bot commented Feb 19, 2026

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview, Comment Feb 19, 2026 9:55am

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors the admin application from container-based components to page-level components that delegate rendering to exported view components in a shared library. It removes legacy container implementations, introduces new page wrappers for routing integration, adds shared utilities and components, updates import paths throughout, and modifies dependency exports to support the new architecture.

Changes

Cohort / File(s) Summary
Container Component Removal
web/apps/admin/src/containers/webhooks/index.tsx
Deleted the legacy WebhooksList container component and all related imports; functionality migrated to page-level WebhooksPage.
New Page Components
web/apps/admin/src/pages/admins/AdminsPage.tsx, web/apps/admin/src/pages/plans/PlansPage.tsx, web/apps/admin/src/pages/preferences/PreferencesPage.tsx, web/apps/admin/src/pages/users/UsersPage.tsx, web/apps/admin/src/pages/webhooks/WebhooksPage.tsx, web/apps/admin/src/pages/organizations/list/index.tsx, web/apps/admin/src/pages/organizations/details/index.tsx
Introduced new page components that wire URL params, context, and callbacks to corresponding view components. Each handles routing integration and data orchestration.
Routing Updates
web/apps/admin/src/routes.tsx
Replaced legacy container and page imports with new page components; updated route element mappings to use PlansPage, PreferencesPage, AdminsPage, WebhooksPage, OrganizationListPage, and OrganizationDetailsPage; adjusted nested route structures accordingly.
Shared View Components & Exports
web/lib/admin/views/admins/index.tsx, web/lib/admin/views/plans/index.tsx, web/lib/admin/views/webhooks/webhooks/index.tsx, web/lib/admin/views/preferences/PreferencesView.tsx, web/lib/admin/views/users/UsersView.tsx, web/lib/admin/views/organizations/list/index.tsx, web/lib/admin/views/organizations/details/index.tsx, web/lib/admin/index.ts
Added/exported new view components (PlansView, WebhooksView, PreferencesView, UsersView, AdminsView, OrganizationList, OrganizationDetails) as reusable library components; updated public API surface with new type definitions and prop interfaces.
Organization Details Refactor
web/lib/admin/views/organizations/details/index.tsx, web/lib/admin/views/organizations/details/contexts/organization-context.tsx, web/lib/admin/views/organizations/details/edit/organization.tsx, web/lib/admin/views/organizations/details/layout/navbar.tsx, web/lib/admin/views/organizations/details/layout/add-tokens-dialog.tsx
Introduced new OrganizationDetails component with comprehensive data fetching; extended OrganizationContext with appUrl, tokenProductId, countries, organizationTypes; replaced AppContext dependency with OrganizationContext throughout; added onExportMembers, onExportProjects, onExportTokens callbacks to navbar and layout.
Organization List Refactor
web/lib/admin/views/organizations/list/index.tsx, web/lib/admin/views/organizations/list/navbar.tsx, web/lib/admin/views/organizations/list/create.tsx
Added new OrganizationList component with infinite query pagination; updated CreateOrganizationPanel to accept appUrl, countries, organizationTypes props; refactored navbar to use onExportCsv callback instead of internal client logic.
Webhooks View & Dialog Updates
web/lib/admin/views/webhooks/webhooks/index.tsx, web/lib/admin/views/webhooks/webhooks/create/index.tsx, web/lib/admin/views/webhooks/webhooks/update/index.tsx, web/lib/admin/views/webhooks/webhooks/header.tsx
Introduced WebhooksView component; added CreateWebhooksProps and UpdateWebhooksProps for prop-driven dialog control; updated header to accept onOpenCreate callback; refactored dialogs to use prop-based navigation instead of internal routing.
Users View & Details Refactor
web/lib/admin/views/users/UsersView.tsx, web/lib/admin/views/users/details/user-details.tsx, web/lib/admin/views/users/list/list.tsx, web/lib/admin/views/users/list/navbar.tsx
Added UsersView component; split UserDetails into UserDetailContent and UserDetailsByUserId; updated UsersList to accept onExportUsers and onNavigateToUser callbacks; refactored navbar to use external export callback instead of internal client logic.
Preferences View Refactor
web/lib/admin/views/preferences/PreferencesView.tsx, web/lib/admin/views/preferences/index.tsx, web/lib/admin/views/preferences/details.tsx
Added new PreferencesView component with dual-query data fetching; converted PreferencesList to prop-driven API; refactored PreferenceDetails to accept preferences, traits, and callbacks as props.
Plans View Refactor
web/lib/admin/views/plans/index.tsx, web/lib/admin/views/plans/details.tsx
Introduced PlansViewProps with selectedPlanId and callbacks; converted PlanDetails to accept plan prop instead of using hook-based data retrieval; replaced Outlet/context pattern with direct prop passing.
Import Path Migrations
web/lib/admin/views/organizations/details/apis/*.tsx, web/lib/admin/views/organizations/details/invoices/*.tsx, web/lib/admin/views/organizations/details/members/*.tsx, web/lib/admin/views/organizations/details/projects/*.tsx, web/lib/admin/views/organizations/details/security/*.tsx, web/lib/admin/views/organizations/details/side-panel/*.tsx, web/lib/admin/views/organizations/details/tokens/*.tsx, web/lib/admin/views/organizations/list/columns.tsx, web/lib/admin/views/plans/columns.tsx, web/lib/admin/views/preferences/columns.tsx, web/lib/admin/views/users/details/layout/*.tsx, web/lib/admin/views/users/details/security/sessions/index.tsx, web/lib/admin/views/users/list/columns.tsx, web/lib/admin/views/users/list/invite-users.tsx, web/lib/admin/views/webhooks/webhooks/columns.tsx
Updated import paths from alias-based paths (~/) to relative paths (../../../../...) and from default imports to named imports for PageTitle, PageHeader, connect-timestamp utilities, constants; switched from absolute component paths to relative imports.
New Shared Components & Utilities
web/lib/admin/assets/icons/UsersIcon.tsx, web/lib/admin/components/AssignRole.tsx, web/lib/admin/components/CustomField.tsx, web/lib/admin/utils/helper.ts, web/lib/admin/utils/connect-timestamp.ts, web/lib/admin/utils/constants.ts
Added UsersIcon SVG component; introduced AssignRole dialog component for role management; exported new utility functions (capitalizeFirstLetter, converBillingAddressToString, timestampToDayjs); added new constants (SCOPES, DEFAULT_ROLES, NULL_DATE, Config interface) and helpers.
Icon & Component Updates
web/lib/admin/views/organizations/details/side-panel/tokens-details-section.tsx, web/lib/admin/views/organizations/details/projects/members/index.tsx, web/lib/admin/views/users/details/layout/navbar.tsx
Replaced CoinIcon/CoinColoredIcon with MixIcon from @radix-ui/react-icons; switched from UserIcon SVG to UsersIcon component; updated icon import paths.
Build Configuration & Dependencies
web/lib/tsup.config.ts, web/lib/package.json, web/apps/admin/vite.config.ts
Expanded external dependencies in tsup config to include react-dom, zod, @radix-ui/react-form, sonner, react-loading-skeleton, @stitches/react, usehooks-ts; added target: 'es2020'; added new dev dependencies to package.json.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Export admin pages as components #1345 — Main changes implement library-oriented refactoring from app container components to router-agnostic exported pages, directly addressing the "Export admin pages as components" issue objective.

Possibly related PRs

Suggested labels

do-not-merge, refactor, admin

Suggested reviewers

  • rohanchkrabrty
  • rsbh

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

coveralls commented Feb 19, 2026

Pull Request Test Coverage Report for Build 22176826685

Details

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 38.457%

Totals Coverage Status
Change from base Build 22176755919: 0.0%
Covered Lines: 16200
Relevant Lines: 42125

💛 - Coveralls

Copy link

@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: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
web/lib/admin/views/organizations/details/edit/organization.tsx (1)

97-102: ⚠️ Potential issue | 🟡 Minor

Form defaultValues are computed once at mount with a potentially empty industries list.

getDefaultValue(organization, industries) classifies an org's metadata.type as "other" whenever industries doesn't contain it (Lines 63-66). If organizationTypes arrives from OrganizationContext after the component mounts (e.g., async fetch), the initial type field will be "other" even for a valid industry, and useForm won't re-initialize once data arrives.

Consider calling form.reset(getDefaultValue(organization, industries)) inside a useEffect when both organization and a non-empty industries list are available:

🛡️ Suggested fix
+const { handleSubmit, control, setError, watch, register, reset,
+  formState: { errors },
+} = useForm<OrgUpdateSchema>({
-  defaultValues: organization
-    ? getDefaultValue(organization, industries)
-    : {},
+  defaultValues: {},
   resolver: zodResolver(orgUpdateSchema),
 });

+useEffect(() => {
+  if (organization && industries.length > 0) {
+    reset(getDefaultValue(organization, industries));
+  }
+}, [organization, industries, reset]);
web/lib/admin/views/webhooks/webhooks/create/index.tsx (1)

22-30: ⚠️ Potential issue | 🟡 Minor

Validation message does not match the constraint.

Line 27: min(3) requires 3+ characters, but the error message says "Must be 10 or more characters long." This will confuse users when they enter, e.g., a 5-character description and get told it must be 10+.

🐛 Proposed fix
   description: z
     .string()
     .trim()
-    .min(3, { message: "Must be 10 or more characters long" }),
+    .min(3, { message: "Must be 3 or more characters long" }),
web/lib/admin/views/webhooks/webhooks/update/index.tsx (1)

25-27: ⚠️ Potential issue | 🟡 Minor

Validation message contradicts the actual constraint.

min(3) enforces a minimum of 3 characters, but the error message states "Must be 10 or more characters long". This is pre-existing but user-facing — the message will confuse users when a 5-character description is rejected with a "10 or more" hint.

🔧 Proposed fix
   description: z
     .string()
     .trim()
-    .min(3, { message: "Must be 10 or more characters long" }),
+    .min(3, { message: "Must be 3 or more characters long" }),
web/lib/admin/views/organizations/details/members/index.tsx (1)

190-196: ⚠️ Potential issue | 🟡 Minor

userToRemove parameter is declared but never used.

The function accepts the evicted user object but only calls invalidateMembersQuery(). Either remove the parameter or use it (e.g., for an optimistic cache update) to avoid misleading callers.

🔧 Proposed fix
-async function removeMember(
-  userToRemove: SearchOrganizationUsersResponse_OrganizationUser,
-) {
+async function removeMember(
+  _userToRemove: SearchOrganizationUsersResponse_OrganizationUser,
+) {

Or simply drop the parameter if the RemoveMember component's onRemove contract doesn't require it.

web/lib/admin/views/organizations/list/navbar.tsx (1)

44-54: ⚠️ Potential issue | 🟡 Minor

Missing user-facing error feedback on export failure.

The catch block only calls console.error, silently swallowing failures from the user's perspective. The org-details navbar (same PR) uses toast.error("Failed to export ...") — this should match for consistency.

🔧 Proposed fix
+import { toast } from "@raystack/apsara";

  async function onDownloadClick() {
    if (!onExportCsv) return;
    try {
      setIsDownloading(true);
      await onExportCsv();
    } catch (error) {
+     toast.error("Failed to export organizations");
      console.error(error);
    } finally {
      setIsDownloading(false);
    }
  }
🧹 Nitpick comments (24)
web/lib/admin/views/organizations/details/side-panel/tokens-details-section.tsx (1)

3-3: MixIcon is semantically unrelated to tokens and weakens the "Available vs. Used" visual distinction.

MixIcon (a crossfade/shuffle glyph from Radix) carries no coin/token semantics and could confuse users scanning the panel. Additionally, the previous pair CoinColoredIcon / CoinIcon provided an immediate affordance distinguishing "Available" (vibrant) from "Used" (muted); now both rows render the same glyph with only a subtle color difference (var(--rs-color-foreground-base-tertiary) vs. inherited default), which is harder to parse at a glance.

Consider using an icon with token/currency semantics — for example TokensIcon, CubeIcon, or whichever coin/credit icon is currently canonical in the design system — and preserving the colored-vs-muted distinction between the two rows.

Also applies to: 55-55, 70-70

web/lib/package.json (2)

99-99: zod: ^3.22.3 — consider aligning with Zod 4, which ships significant improvements.

The provided library documentation describes Zod 4 as a major revision with a unified error API, safer number/string validation, improved performance, and a modular z.core architecture. If the admin view code is new and not yet accumulated Zod-specific usage, migrating to "zod": "^4.0.0" now is lower friction than doing so later.


95-95: Replace the unmaintained @stitches/react dependency with an alternative.

@stitches/react has been unmaintained for ~4 years (last stable release April 2022). Since it's marked as external in the build config and published as a peer dependency in @raystack/frontier/admin, consumers inherit this dead dependency. The library is used minimally (only for the CSS type in SheetFooter.tsx). Replace it with an actively maintained alternative such as vanilla CSS modules, Tailwind, or @emotion/styled.

web/lib/admin/utils/connect-timestamp.ts (1)

9-12: timestampToDayjs bypasses the epoch-sentinel guard defined in the same file.

isNullTimestamp treats seconds <= 0 as "no date set" (per its JSDoc), but timestampToDayjs only checks for null/undefined. A Timestamp with seconds = 0 passes through timestampToDate as new Date(0) (truthy), causing the helper to return a valid dayjs object representing 1970-01-01 instead of null. Callers that use timestampToDayjs(ts) ? as the null guard will render "01 Jan 1970" rather than a dash.

🛡️ Suggested fix — apply `isNullTimestamp` before converting
 export function timestampToDayjs(timestamp?: Timestamp): Dayjs | null {
-  const date = timestampToDate(timestamp);
-  return date ? dayjs(date) : null;
+  if (isNullTimestamp(timestamp)) return null;
+  const date = timestampToDate(timestamp);
+  return date ? dayjs(date) : null;
 }
web/lib/admin/views/organizations/details/edit/organization.tsx (2)

81-88: countries state duplicates countriesFromContext without local mutation — the state + effect can be eliminated.

The component never modifies the countries array locally (no setCountries call beyond the sync effect), so countriesFromContext can be used directly in the render, removing one layer of indirection.

♻️ Suggested simplification
-const [countries, setCountries] = useState<string[]>(countriesFromContext);
 const queryClient = useQueryClient();
 const transport = useTransport();
 const orgId = organization?.id || "";

-useEffect(() => {
-  if (countriesFromContext.length > 0) setCountries(countriesFromContext);
-}, [countriesFromContext]);

Then replace countries.map(...) with countriesFromContext.map(...) on Line 285.


30-30: parseInt without a radix argument.

While this defaults to base 10 and the value comes from a type="number" input, explicitly passing the radix removes ambiguity and satisfies radix lint rules.

🛠️ Suggested fix
-size: z.string().transform((value) => parseInt(value)),
+size: z.string().transform((value) => parseInt(value, 10)),
web/lib/admin/assets/icons/UsersIcon.tsx (1)

13-19: Hardcoded id attributes will produce duplicate DOM IDs when multiple UsersIcon instances render on the same page.

id="users" and id="Vector" are static strings. While there are no url(#...) references in this SVG so there's no visual breakage, duplicate IDs are invalid HTML and can interfere with CSS selectors and accessibility tooling.

♻️ Suggested fix – remove unnecessary IDs
-      <g id="users">
+      <g>
         <path
-          id="Vector"
           d="M5.6 6.4C6.23652 ..."
           fill="currentColor"
         />
       </g>
web/lib/admin/views/webhooks/webhooks/create/index.tsx (1)

96-96: Inconsistent test-id attribute naming.

Line 96 uses data-testid (React Testing Library convention) while Line 132 still uses data-test-id. Pick one convention and apply it consistently.

Also applies to: 132-132

web/lib/admin/views/users/details/layout/membership-dropdown.tsx (1)

85-96: Pre-existing: Object.assign mutates the user context object.

Line 89: Object.assign(user ?? {}, { roleNames: ... }) mutates user in-place when user is truthy. Since user comes from useUser() context, this silently mutates shared state. Consider using a spread instead:

-        Object.assign(user ?? {}, {
+        { ...(user ?? {}),
           roleNames: data?.roleNames || [],
           roleTitles: data?.roleTitles || [],
           roleIds: data?.roleIds || [],
-        }),
+        },

Low priority since it's pre-existing, but worth fixing while this area is being touched.

web/lib/admin/views/plans/details.tsx (1)

22-29: Consider extracting the IIFE into a local variable for readability.

♻️ Suggested simplification
+  const createdAtFormatted = (() => {
+    const date = timestampToDate(plan?.createdAt);
+    return date
+      ? date.toLocaleString("en", { month: "long", day: "numeric", year: "numeric" })
+      : "-";
+  })();
+
   return (
     ...
         <Text size={1}>
-            {(() => {
-              const date = timestampToDate(plan?.createdAt);
-              return date ? date.toLocaleString("en", {
-                month: "long",
-                day: "numeric",
-                year: "numeric",
-              }) : "-";
-            })()}
+            {createdAtFormatted}
         </Text>
web/lib/admin/views/organizations/list/index.tsx (1)

150-153: Including isFetchingNextPage in loading state may cause full-table loading flash during pagination.

Line 153 combines isFetchingNextPage with isLoading, which passes a loading state to DataTable even when only appending the next page. This could cause the entire table to show a loading indicator (or lose interactivity) during infinite scroll, degrading UX.

Consider separating initial load from pagination load:

♻️ Proposed change
- const loading = isLoading || isPlansLoading || isFetchingNextPage;
+ const loading = isLoading || isPlansLoading;

If the DataTable component supports a separate isFetchingMore prop or a footer loading indicator, use that for isFetchingNextPage instead.

web/lib/admin/views/organizations/list/create.tsx (1)

61-68: Unnecessary local state mirrors the prop — use the prop directly.

The countries local state is initialized from countriesProp and synced via useEffect, but the sync has a subtle gap: it won't update if countriesProp becomes empty (Line 65 guards on length > 0). More importantly, this "sync prop to state" pattern is a known React anti-pattern. Since there's no local mutation of the countries list, use countriesProp directly (or alias it):

♻️ Proposed simplification
- const [countries, setCountries] = useState<string[]>(countriesProp);
  const industries = organizationTypes;
-
- useEffect(() => {
-   if (countriesProp.length > 0) {
-     setCountries(countriesProp);
-   }
- }, [countriesProp]);
+ const countries = countriesProp;
+ const industries = organizationTypes;
web/lib/admin/views/webhooks/webhooks/index.tsx (3)

40-43: openEditPage silently no-ops when onSelectWebhook is absent.

If the consumer doesn't pass onSelectWebhook, clicking "Update" in the row action dropdown does nothing with no feedback. Consider either disabling the Update action when the callback is missing (similar to how enableDelete gates the Delete action) or making onSelectWebhook required.


83-89: Both Create and Update sheets can render simultaneously.

If createOpen and selectedWebhookId are both truthy, two side-sheets will overlay. The parent page likely prevents this, but there's no guard here. A simple mutual-exclusion check would make this component more robust.

💡 Example guard
-      {createOpen && <CreateWebhooks onClose={onCloseDetail} />}
-      {selectedWebhookId && (
+      {createOpen && !selectedWebhookId && <CreateWebhooks onClose={onCloseDetail} />}
+      {selectedWebhookId && !createOpen && (
         <UpdateWebhooks
           webhookId={selectedWebhookId}
           onClose={onCloseDetail}
         />
       )}

45-57: console.error fires on every re-render in error state.

This console.error sits inside the render body, so it will log repeatedly on every re-render while isError is true. Consider moving it to a useEffect keyed on error if you want to retain it for debugging.

web/apps/admin/src/pages/plans/PlansPage.tsx (1)

4-14: Follows the page-wrapper pattern well.

Minor nit: onCloseDetail is an inline closure, creating a new reference each render, whereas the sibling UsersPage wraps similar callbacks in useCallback. Not functionally broken, but worth aligning for consistency if PlansView ever gets memoized.

web/apps/admin/src/pages/users/UsersPage.tsx (1)

13-15: No error handling for the export stream.

If exportCsvFromStream or the underlying gRPC stream fails, the rejected promise from this async callback will surface as an unhandled rejection. Consider wrapping in try/catch with a user-facing toast so export failures are communicated.

💡 Proposed improvement
  const onExportUsers = useCallback(async () => {
-    await exportCsvFromStream(adminClient.exportUsers, {}, "users.csv");
+    try {
+      await exportCsvFromStream(adminClient.exportUsers, {}, "users.csv");
+    } catch (err) {
+      console.error("Failed to export users:", err);
+      toast.error("Failed to export users");
+    }
  }, []);
web/lib/admin/views/organizations/details/layout/navbar.tsx (1)

24-24: Merge InputField into the existing @raystack/apsara import block.

InputField is imported in a separate statement while @raystack/apsara is already imported at lines 3-14.

♻️ Proposed fix
 import {
   Flex,
   Text,
   Breadcrumb,
   Avatar,
   IconButton,
   DropdownMenu,
   Chip,
   Spinner,
   getAvatarColor,
   toast,
+  InputField,
 } from "@raystack/apsara";

-import { InputField } from "@raystack/apsara";
web/lib/admin/views/preferences/PreferencesView.tsx (1)

47-61: Move console.error out of the render body.

Calling console.error during render is a side effect. In React 18 StrictMode (development), components render twice per commit, causing this to fire twice per error state change. Moving it to a useEffect ensures it fires exactly once when the error state becomes truthy.

♻️ Suggested fix
+  useEffect(() => {
+    if (isError) console.error("ConnectRPC Error:", error);
+  }, [isError, error]);
+
   if (isError) {
-    console.error("ConnectRPC Error:", error);
     return (
web/lib/admin/views/organizations/details/index.tsx (1)

126-126: roles is recreated on every render — wrap in useMemo.

Every render produces a new array reference, causing all OrganizationContext consumers that read roles to re-render unnecessarily.

♻️ Proposed fix
-  const roles = [...defaultRoles, ...organizationRoles];
+  const roles = useMemo(
+    () => [...defaultRoles, ...organizationRoles],
+    [defaultRoles, organizationRoles],
+  );
web/lib/admin/views/plans/index.tsx (1)

40-40: Redundant ?? []plans is already [] by line 39.

-  const planMapById = reduceByKey(plans ?? [], "id");
+  const planMapById = reduceByKey(plans, "id");
web/apps/admin/src/routes.tsx (1)

74-77: Self-parent route pattern is functional but consider flattening for clarity.

<Route path="users" element={<UsersPage />}>
  <Route path=":userId" element={<UsersPage />} />
  <Route path=":userId/security" element={<UsersPage />} />
</Route>

This works because React Router v6 merges matched params up the hierarchy, so the parent <UsersPage /> reads :userId via useParams() even though it's defined on a child. The child element={<UsersPage />} is never mounted (no <Outlet /> in UsersPage); it only exists to mark the path as valid. The same pattern is applied for plans, preferences, and webhooks.

A cleaner equivalent that makes intent explicit:

<Route path="users">
  <Route index element={<UsersPage />} />
  <Route path=":userId" element={<UsersPage />} />
  <Route path=":userId/security" element={<UsersPage />} />
</Route>

This removes the silent parent element, avoids the implicit param-inheritance dependency, and makes the routes self-documenting.

web/apps/admin/src/pages/organizations/details/index.tsx (1)

12-15: Extract loadCountries to a shared utility to eliminate duplication.

The function is identical in both details/index.tsx and list/index.tsx (lines 12-15). Move it to ~/utils/countries.ts or add it to the existing ~/utils/helper.ts.

web/apps/admin/src/pages/organizations/list/index.tsx (1)

22-24: Missing error handling for loadCountries.

If the dynamic import fails (e.g., bundler misconfiguration, missing asset), the rejected promise is unhandled and countries silently remain []. Consider adding a .catch to log the error or surface it.

Proposed fix
  useEffect(() => {
-   loadCountries().then(setCountries);
+   loadCountries().then(setCountries).catch(console.error);
  }, []);

Comment on lines +26 to +51
const onExportMembers = useCallback(async () => {
if (!organizationId) return;
queryClient.setQueryData(
createConnectQueryKey({
schema: FrontierServiceQueries.getOrganizationKyc,
transport,
input: { orgId: organizationId },
cardinality: "finite",
}),
create(GetOrganizationKycResponseSchema, { organizationKyc: kyc }),
await exportCsvFromStream(
adminClient.exportOrganizationUsers,
{ id: organizationId },
"organization-members.csv",
);
}
}, [organizationId]);

// Fetch default roles
const {
data: defaultRoles = [],
isLoading: isDefaultRolesLoading,
error: defaultRolesError,
} = useQuery(
FrontierServiceQueries.listRoles,
{ scopes: [ORG_NAMESPACE] },
{
enabled: !!organizationId,
select: (data) => data?.roles || [],
},
);

// Fetch organization-specific roles
const {
data: organizationRoles = [],
isLoading: isOrgRolesLoading,
error: orgRolesError,
} = useQuery(
FrontierServiceQueries.listOrganizationRoles,
{ orgId: organizationId || "", scopes: [ORG_NAMESPACE] },
{
enabled: !!organizationId,
select: (data) => data?.roles || [],
},
);

const roles = [...defaultRoles, ...organizationRoles];

// Fetch organization members
const {
data: orgMembersMap = {},
isLoading: isOrgMembersMapLoading,
error: orgMembersError,
} = useQuery(
FrontierServiceQueries.listOrganizationUsers,
{ id: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => {
const users = data?.users || [];
return users.reduce(
(acc, user) => {
const id = user.id || "";
acc[id] = user;
return acc;
},
{} as Record<string, User>,
);
},
},
);

// Fetch billing accounts list
const { data: firstBillingAccountId = "", error: billingAccountsError } =
useQuery(
FrontierServiceQueries.listBillingAccounts,
{ orgId: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => data?.billingAccounts?.[0]?.id || "",
},
const onExportProjects = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationProjects,
{ id: organizationId },
"organization-projects.csv",
);
}, [organizationId]);

// Fetch billing account details
const {
data: billingAccountData,
isLoading: isBillingAccountLoading,
error: billingAccountError,
refetch: fetchBillingAccountDetails,
} = useQuery(
FrontierServiceQueries.getBillingAccount,
{
orgId: organizationId || "",
id: firstBillingAccountId,
withBillingDetails: true,
},
{
enabled: !!organizationId && !!firstBillingAccountId,
select: (data) => ({
billingAccount: data?.billingAccount,
billingAccountDetails: data?.billingDetails,
}),
},
);

const billingAccount = billingAccountData?.billingAccount;
const billingAccountDetails = billingAccountData?.billingAccountDetails;

// Fetch billing balance
const {
data: tokenBalance = "0",
isLoading: isTokenBalanceLoading,
error: tokenBalanceError,
refetch: fetchTokenBalance,
} = useQuery(
FrontierServiceQueries.getBillingBalance,
{
orgId: organizationId || "",
id: firstBillingAccountId,
},
{
enabled: !!organizationId && !!firstBillingAccountId,
select: (data) => String(data?.balance?.amount || "0"),
},
);

// Error handling
useEffect(() => {
if (organizationError) {
console.error("Failed to fetch organization:", organizationError);
}
if (kycError) {
console.error("Failed to fetch KYC details:", kycError);
}
if (defaultRolesError) {
console.error("Failed to fetch default roles:", defaultRolesError);
}
if (orgRolesError) {
console.error("Failed to fetch organization roles:", orgRolesError);
}
if (orgMembersError) {
console.error("Failed to fetch organization members:", orgMembersError);
}
if (billingAccountsError) {
console.error("Failed to fetch billing accounts:", billingAccountsError);
}
if (billingAccountError) {
console.error(
"Failed to fetch billing account details:",
billingAccountError,
);
}
if (tokenBalanceError) {
console.error("Failed to fetch token balance:", tokenBalanceError);
}
}, [
organizationError,
kycError,
defaultRolesError,
orgRolesError,
orgMembersError,
billingAccountsError,
billingAccountError,
tokenBalanceError,
]);
const onExportTokens = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationTokens,
{ id: organizationId },
"organization-tokens.csv",
);
}, [organizationId]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Export callbacks have no error handling — failures are silent.

If exportCsvFromStream throws (network error, streaming failure, etc.), the promise rejection is unhandled. No user-facing feedback is shown.

🐛 Proposed fix for each callback (pattern shown for `onExportMembers`)
  const onExportMembers = useCallback(async () => {
    if (!organizationId) return;
-   await exportCsvFromStream(
-     adminClient.exportOrganizationUsers,
-     { id: organizationId },
-     "organization-members.csv",
-   );
+   try {
+     await exportCsvFromStream(
+       adminClient.exportOrganizationUsers,
+       { id: organizationId },
+       "organization-members.csv",
+     );
+   } catch (err) {
+     console.error("Failed to export members:", err);
+     // show toast or surface error to user
+   }
  }, [organizationId]);
📝 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 onExportMembers = useCallback(async () => {
if (!organizationId) return;
queryClient.setQueryData(
createConnectQueryKey({
schema: FrontierServiceQueries.getOrganizationKyc,
transport,
input: { orgId: organizationId },
cardinality: "finite",
}),
create(GetOrganizationKycResponseSchema, { organizationKyc: kyc }),
await exportCsvFromStream(
adminClient.exportOrganizationUsers,
{ id: organizationId },
"organization-members.csv",
);
}
}, [organizationId]);
// Fetch default roles
const {
data: defaultRoles = [],
isLoading: isDefaultRolesLoading,
error: defaultRolesError,
} = useQuery(
FrontierServiceQueries.listRoles,
{ scopes: [ORG_NAMESPACE] },
{
enabled: !!organizationId,
select: (data) => data?.roles || [],
},
);
// Fetch organization-specific roles
const {
data: organizationRoles = [],
isLoading: isOrgRolesLoading,
error: orgRolesError,
} = useQuery(
FrontierServiceQueries.listOrganizationRoles,
{ orgId: organizationId || "", scopes: [ORG_NAMESPACE] },
{
enabled: !!organizationId,
select: (data) => data?.roles || [],
},
);
const roles = [...defaultRoles, ...organizationRoles];
// Fetch organization members
const {
data: orgMembersMap = {},
isLoading: isOrgMembersMapLoading,
error: orgMembersError,
} = useQuery(
FrontierServiceQueries.listOrganizationUsers,
{ id: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => {
const users = data?.users || [];
return users.reduce(
(acc, user) => {
const id = user.id || "";
acc[id] = user;
return acc;
},
{} as Record<string, User>,
);
},
},
);
// Fetch billing accounts list
const { data: firstBillingAccountId = "", error: billingAccountsError } =
useQuery(
FrontierServiceQueries.listBillingAccounts,
{ orgId: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => data?.billingAccounts?.[0]?.id || "",
},
const onExportProjects = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationProjects,
{ id: organizationId },
"organization-projects.csv",
);
}, [organizationId]);
// Fetch billing account details
const {
data: billingAccountData,
isLoading: isBillingAccountLoading,
error: billingAccountError,
refetch: fetchBillingAccountDetails,
} = useQuery(
FrontierServiceQueries.getBillingAccount,
{
orgId: organizationId || "",
id: firstBillingAccountId,
withBillingDetails: true,
},
{
enabled: !!organizationId && !!firstBillingAccountId,
select: (data) => ({
billingAccount: data?.billingAccount,
billingAccountDetails: data?.billingDetails,
}),
},
);
const billingAccount = billingAccountData?.billingAccount;
const billingAccountDetails = billingAccountData?.billingAccountDetails;
// Fetch billing balance
const {
data: tokenBalance = "0",
isLoading: isTokenBalanceLoading,
error: tokenBalanceError,
refetch: fetchTokenBalance,
} = useQuery(
FrontierServiceQueries.getBillingBalance,
{
orgId: organizationId || "",
id: firstBillingAccountId,
},
{
enabled: !!organizationId && !!firstBillingAccountId,
select: (data) => String(data?.balance?.amount || "0"),
},
);
// Error handling
useEffect(() => {
if (organizationError) {
console.error("Failed to fetch organization:", organizationError);
}
if (kycError) {
console.error("Failed to fetch KYC details:", kycError);
}
if (defaultRolesError) {
console.error("Failed to fetch default roles:", defaultRolesError);
}
if (orgRolesError) {
console.error("Failed to fetch organization roles:", orgRolesError);
}
if (orgMembersError) {
console.error("Failed to fetch organization members:", orgMembersError);
}
if (billingAccountsError) {
console.error("Failed to fetch billing accounts:", billingAccountsError);
}
if (billingAccountError) {
console.error(
"Failed to fetch billing account details:",
billingAccountError,
);
}
if (tokenBalanceError) {
console.error("Failed to fetch token balance:", tokenBalanceError);
}
}, [
organizationError,
kycError,
defaultRolesError,
orgRolesError,
orgMembersError,
billingAccountsError,
billingAccountError,
tokenBalanceError,
]);
const onExportTokens = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationTokens,
{ id: organizationId },
"organization-tokens.csv",
);
}, [organizationId]);
const onExportMembers = useCallback(async () => {
if (!organizationId) return;
try {
await exportCsvFromStream(
adminClient.exportOrganizationUsers,
{ id: organizationId },
"organization-members.csv",
);
} catch (err) {
console.error("Failed to export members:", err);
// show toast or surface error to user
}
}, [organizationId]);
const onExportProjects = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationProjects,
{ id: organizationId },
"organization-projects.csv",
);
}, [organizationId]);
const onExportTokens = useCallback(async () => {
if (!organizationId) return;
await exportCsvFromStream(
adminClient.exportOrganizationTokens,
{ id: organizationId },
"organization-tokens.csv",
);
}, [organizationId]);

Comment on lines +27 to +34
import {
OrganizationSecurity,
OrganizationMembersPage,
OrganizationProjectssPage,
OrganizationInvoicesPage,
OrganizationTokensPage,
OrganizationApisPage,
} from "@raystack/frontier/admin";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for OrganizationProject exports and definitions
rg -n "OrganizationProject" web/lib/admin/ -A 2

Repository: raystack/frontier

Length of output: 4890


OrganizationProjectssPage has a double-s typo — rename the component definition and all usages.

The component is defined as OrganizationProjectssPage in web/lib/admin/views/organizations/details/projects/index.tsx (line 71), exported from web/lib/admin/index.ts (line 20), and imported here. Rename to OrganizationProjectsPage (single s in "Projects") throughout both the definition and all imports/exports.

Comment on lines 98 to 136
const onSubmit = async (data: FormData) => {
try {
const client = createClient(FrontierService, transport);
const policiesResp = await client.listPolicies(
create(ListPoliciesRequestSchema, {
orgId: organizationId,
userId: user?.id,
}),
);
const policies = policiesResp.policies || [];

const removedRolesPolicies = policies.filter(
(policy: Policy) => !(policy.roleId && data.roleIds.has(policy.roleId)),
);
await Promise.all(
removedRolesPolicies.map((policy: Policy) =>
deletePolicy(
create(DeletePolicyRequestSchema, { id: policy.id || "" }),
),
),
);

const resource = `app/organization:${organizationId}`;
const principal = `app/user:${user?.id}`;

const assignedRolesArr = Array.from(data.roleIds);
await Promise.all(
assignedRolesArr.map((roleId) =>
createPolicy(
create(CreatePolicyRequestSchema, {
body: {
roleId,
resource,
principal,
},
}),
),
),
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Duplicate policies created for roles that were already assigned.

The submit handler deletes policies for removed roles (Lines 109-118), but then creates policies for all selected roles (Lines 123-136), including those that already had a policy and were not removed. For example, if a user has roles A and B and the admin updates to A and C, the code deletes B's policy but creates policies for both A (duplicate) and C.

🐛 Proposed fix — only create policies for newly added roles
      const removedRolesPolicies = policies.filter(
        (policy: Policy) => !(policy.roleId && data.roleIds.has(policy.roleId)),
      );
      await Promise.all(
        removedRolesPolicies.map((policy: Policy) =>
          deletePolicy(
            create(DeletePolicyRequestSchema, { id: policy.id || "" }),
          ),
        ),
      );

+     const existingRoleIds = new Set(
+       policies
+         .filter((p: Policy) => p.roleId && data.roleIds.has(p.roleId))
+         .map((p: Policy) => p.roleId),
+     );
+
      const resource = `app/organization:${organizationId}`;
      const principal = `app/user:${user?.id}`;

-     const assignedRolesArr = Array.from(data.roleIds);
+     const newRoleIds = Array.from(data.roleIds).filter(
+       (roleId) => !existingRoleIds.has(roleId),
+     );
      await Promise.all(
-       assignedRolesArr.map((roleId) =>
+       newRoleIds.map((roleId) =>
          createPolicy(
            create(CreatePolicyRequestSchema, {
              body: {
                roleId,
                resource,
                principal,
              },
            }),
          ),
        ),
      );
📝 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 onSubmit = async (data: FormData) => {
try {
const client = createClient(FrontierService, transport);
const policiesResp = await client.listPolicies(
create(ListPoliciesRequestSchema, {
orgId: organizationId,
userId: user?.id,
}),
);
const policies = policiesResp.policies || [];
const removedRolesPolicies = policies.filter(
(policy: Policy) => !(policy.roleId && data.roleIds.has(policy.roleId)),
);
await Promise.all(
removedRolesPolicies.map((policy: Policy) =>
deletePolicy(
create(DeletePolicyRequestSchema, { id: policy.id || "" }),
),
),
);
const resource = `app/organization:${organizationId}`;
const principal = `app/user:${user?.id}`;
const assignedRolesArr = Array.from(data.roleIds);
await Promise.all(
assignedRolesArr.map((roleId) =>
createPolicy(
create(CreatePolicyRequestSchema, {
body: {
roleId,
resource,
principal,
},
}),
),
),
);
const onSubmit = async (data: FormData) => {
try {
const client = createClient(FrontierService, transport);
const policiesResp = await client.listPolicies(
create(ListPoliciesRequestSchema, {
orgId: organizationId,
userId: user?.id,
}),
);
const policies = policiesResp.policies || [];
const removedRolesPolicies = policies.filter(
(policy: Policy) => !(policy.roleId && data.roleIds.has(policy.roleId)),
);
await Promise.all(
removedRolesPolicies.map((policy: Policy) =>
deletePolicy(
create(DeletePolicyRequestSchema, { id: policy.id || "" }),
),
),
);
const existingRoleIds = new Set(
policies
.filter((p: Policy) => p.roleId && data.roleIds.has(p.roleId))
.map((p: Policy) => p.roleId),
);
const resource = `app/organization:${organizationId}`;
const principal = `app/user:${user?.id}`;
const newRoleIds = Array.from(data.roleIds).filter(
(roleId) => !existingRoleIds.has(roleId),
);
await Promise.all(
newRoleIds.map((roleId) =>
createPolicy(
create(CreatePolicyRequestSchema, {
body: {
roleId,
resource,
principal,
},
}),
),
),
);

Comment on lines +24 to +28
export const defaultConfig: Config = {
title: "Frontier Admin",
app_url: "example.com",
token_product_id: "token",
organization_types: [],
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Placeholder defaults in defaultConfig may cause silent production misconfiguration.

app_url: "example.com" and token_product_id: "token" are RFC-2606 example values. Any consumer that forgets to pass the real config (or provides a partial override) will silently produce broken app links and billing calls referencing a non-existent product ID "token", with no runtime warning.

Prefer empty strings or undefined to fail loudly rather than silently routing to a fictitious domain/product:

🛡️ Proposed fix
 export const defaultConfig: Config = {
   title: "Frontier Admin",
-  app_url: "example.com",
-  token_product_id: "token",
+  app_url: "",
+  token_product_id: "",
   organization_types: [],
 };
📝 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
export const defaultConfig: Config = {
title: "Frontier Admin",
app_url: "example.com",
token_product_id: "token",
organization_types: [],
export const defaultConfig: Config = {
title: "Frontier Admin",
app_url: "",
token_product_id: "",
organization_types: [],

return str.charAt(0).toUpperCase() + str.slice(1);
}

export function converBillingAddressToString(
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all usages of the misspelled function name to assess rename scope
rg -n "converBillingAddressToString" --type=ts --type=tsx
rg -n "converBillingAddressToString"

Repository: raystack/frontier

Length of output: 657


🏁 Script executed:

cat -n web/lib/react/utils/index.ts | head -35

Repository: raystack/frontier

Length of output: 1232


🏁 Script executed:

rg -n "from.*react/utils" --type=ts --type=tsx
rg -n "from.*react/utils"

Repository: raystack/frontier

Length of output: 4472


🏁 Script executed:

rg -n "converBillingAddressToString" | grep -E "(import|from)"

Repository: raystack/frontier

Length of output: 216


🏁 Script executed:

rg -n "from.*src/utils/helper" web/apps/admin/

Repository: raystack/frontier

Length of output: 43


🏁 Script executed:

rg -n "from.*utils/helper" web/apps/admin/

Repository: raystack/frontier

Length of output: 587


Typo in function name: converBillingAddressToStringconvertBillingAddressToString.

Missing "t" in "convert". This typo exists in two lib export locations. Since these are newly exported public APIs, now is the cheapest time to fix it. The same typo exists in the apps version (web/apps/admin/src/utils/helper.ts), but it has no callers.

Files to update
  1. web/lib/admin/utils/helper.ts line 25:
-export function converBillingAddressToString(
+export function convertBillingAddressToString(
  1. web/lib/react/utils/index.ts line 26:
-export const converBillingAddressToString = (
+export const convertBillingAddressToString = (
  1. web/lib/admin/views/organizations/details/side-panel/billing-details-section.tsx line 3:
-import { converBillingAddressToString } from "../../../../utils/helper";
+import { convertBillingAddressToString } from "../../../../utils/helper";
  1. Same file line 96: Update usage from converBillingAddressToString to convertBillingAddressToString
📝 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
export function converBillingAddressToString(
export function convertBillingAddressToString(

Comment on lines 94 to 99
"@radix-ui/react-form": "^0.0.2",
"@stitches/react": "^1.2.8",
"react-loading-skeleton": "^3.4.0",
"sonner": "^1.4.41",
"usehooks-ts": "^3.1.1",
"zod": "^3.22.3"
Copy link

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:

#!/bin/bash
# Find tsup config files
fd -e ts -e js 'tsup.config' --exec cat {}

Repository: raystack/frontier

Length of output: 1410


🏁 Script executed:

#!/bin/bash
# Read the package.json to see full peerDependencies and dependencies
cat web/lib/package.json | head -150

Repository: raystack/frontier

Length of output: 4142


Add all externally-marked admin dependencies to peerDependencies to prevent runtime failures for library consumers.

Confirmed: The admin entrypoint marks zod, @radix-ui/react-form, sonner, react-loading-skeleton, @stitches/react, and usehooks-ts as external (not bundled), but none appear in peerDependencies. Consumers will encounter missing-module errors at runtime unless they manually install these packages.

The proposed fix is incomplete. It omits @radix-ui/react-form (mentioned in the review text but not included in the diff), and entirely overlooks @stitches/react, zod, and react-dom—all of which are in the admin externals list.

♻️ Corrected peerDependencies addition
   "peerDependencies": {
     "@raystack/apsara": ">=0.30.0",
+    "@radix-ui/react-form": ">=0.0.0",
+    "@stitches/react": ">=1.0.0",
+    "react-dom": "^18.2.0",
+    "react-loading-skeleton": ">=3.0.0",
+    "sonner": ">=1.0.0",
+    "zod": ">=3.0.0",
+    "usehooks-ts": ">=3.0.0",
     "react": "^18.2.0",
     "react-router-dom": ">=6.0.0"
   },

@paanSinghCoder paanSinghCoder changed the base branch from main to feat/export-users-page February 19, 2026 06:49
@paanSinghCoder paanSinghCoder changed the title Feat/export organisations page feat: export Organisations page as a component and consume it in apps/admin Feb 19, 2026
@paanSinghCoder paanSinghCoder self-assigned this Feb 19, 2026
@paanSinghCoder paanSinghCoder added Do not merge Label to indicate that the PR is not ready to be merged even though might be (or not) approvals. and removed Do not merge Label to indicate that the PR is not ready to be merged even though might be (or not) approvals. labels Feb 19, 2026
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.

2 participants

Comments