From 30697f695e54066af5de5416529d70b9d60645b8 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Mon, 16 Feb 2026 09:12:51 +0530 Subject: [PATCH 1/5] feat: move webhooks page --- .../admin/src/containers/webhooks/index.tsx | 63 ------------- .../admin/src/pages/webhooks/WebhooksPage.tsx | 24 +++++ web/apps/admin/src/routes.tsx | 10 +- .../admin}/components/CustomField.tsx | 6 +- .../admin/components/SheetFooter.tsx} | 0 .../multiselect}/multiselect/index.tsx | 0 web/lib/admin/index.ts | 1 + web/lib/admin/utils/helper.ts | 4 + .../admin/utils/webhook-events.ts} | 0 .../views/webhooks}/webhooks/columns.tsx | 4 +- .../views/webhooks}/webhooks/create/index.tsx | 23 +++-- .../views/webhooks}/webhooks/delete/index.tsx | 0 .../admin/views/webhooks}/webhooks/header.tsx | 15 ++- .../webhooks/hooks/useWebhookQueries.ts | 0 .../admin/views/webhooks/webhooks/index.tsx | 92 +++++++++++++++++++ .../views/webhooks}/webhooks/update/index.tsx | 29 +++--- .../webhooks}/webhooks/webhooks.module.css | 0 web/lib/package.json | 7 +- web/lib/tsup.config.ts | 11 ++- web/pnpm-lock.yaml | 15 +++ 20 files changed, 204 insertions(+), 100 deletions(-) delete mode 100644 web/apps/admin/src/containers/webhooks/index.tsx create mode 100644 web/apps/admin/src/pages/webhooks/WebhooksPage.tsx rename web/{apps/admin/src => lib/admin}/components/CustomField.tsx (96%) rename web/{apps/admin/src/components/sheet/footer.tsx => lib/admin/components/SheetFooter.tsx} (100%) rename web/{apps/admin/src/components => lib/admin/components/multiselect}/multiselect/index.tsx (100%) rename web/{apps/admin/src/utils/webhook_events.ts => lib/admin/utils/webhook-events.ts} (100%) rename web/{apps/admin/src/containers => lib/admin/views/webhooks}/webhooks/columns.tsx (98%) rename web/{apps/admin/src/containers => lib/admin/views/webhooks}/webhooks/create/index.tsx (87%) rename web/{apps/admin/src/containers => lib/admin/views/webhooks}/webhooks/delete/index.tsx (100%) rename web/{apps/admin/src/containers => lib/admin/views/webhooks}/webhooks/header.tsx (62%) rename web/{apps/admin/src/containers => lib/admin/views/webhooks}/webhooks/hooks/useWebhookQueries.ts (100%) create mode 100644 web/lib/admin/views/webhooks/webhooks/index.tsx rename web/{apps/admin/src/containers => lib/admin/views/webhooks}/webhooks/update/index.tsx (87%) rename web/{apps/admin/src/containers => lib/admin/views/webhooks}/webhooks/webhooks.module.css (100%) diff --git a/web/apps/admin/src/containers/webhooks/index.tsx b/web/apps/admin/src/containers/webhooks/index.tsx deleted file mode 100644 index c619757e77..0000000000 --- a/web/apps/admin/src/containers/webhooks/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Flex, DataTable, EmptyState } from "@raystack/apsara"; -import { getColumns } from "./columns"; -import { WebhooksHeader } from "./header"; -import { Outlet, useNavigate } from "react-router-dom"; -import styles from "./webhooks.module.css"; -import { useWebhookQueries } from "./hooks/useWebhookQueries"; -import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; -import { AppContext } from "~/contexts/App"; -import { useContext } from "react"; - -export default function WebhooksList() { - const navigate = useNavigate(); - const { config } = useContext(AppContext); - const { - listWebhooks: { - data: webhooksResponse, - isLoading, - error, - isError, - }, - deleteWebhookMutation, - } = useWebhookQueries(); - - const webhooks = webhooksResponse?.webhooks || []; - - function openEditPage(id: string) { - navigate(`/webhooks/${id}`); - } - - if (isError) { - console.error("ConnectRPC Error:", error); - return ( - } - heading="Error Loading Webhooks" - subHeading={ - error?.message || - "Something went wrong while loading webhooks. Please try again." - } - /> - ); - } - - const enableDelete = config.webhooks?.enable_delete ?? false; - const columns = getColumns({ openEditPage, deleteWebhookMutation, enableDelete }); - return ( - - - - - - - - ); -} diff --git a/web/apps/admin/src/pages/webhooks/WebhooksPage.tsx b/web/apps/admin/src/pages/webhooks/WebhooksPage.tsx new file mode 100644 index 0000000000..a467f6a99c --- /dev/null +++ b/web/apps/admin/src/pages/webhooks/WebhooksPage.tsx @@ -0,0 +1,24 @@ +import { useContext } from "react"; +import { useMatch, useParams, useNavigate } from "react-router-dom"; +import { WebhooksView } from "@raystack/frontier/admin"; +import { AppContext } from "~/contexts/App"; + +export function WebhooksPage() { + const { config } = useContext(AppContext); + const { webhookId } = useParams(); + const navigate = useNavigate(); + const isCreate = useMatch("/webhooks/create"); + + const enableDelete = config?.webhooks?.enable_delete ?? false; + + return ( + navigate("/webhooks")} + onSelectWebhook={(id: string) => navigate(`/webhooks/${encodeURIComponent(id)}`)} + onOpenCreate={() => navigate("/webhooks/create")} + enableDelete={enableDelete} + /> + ); +} diff --git a/web/apps/admin/src/routes.tsx b/web/apps/admin/src/routes.tsx index 0cc0fa8da4..7f01d5b49c 100644 --- a/web/apps/admin/src/routes.tsx +++ b/web/apps/admin/src/routes.tsx @@ -21,9 +21,7 @@ import { RolesPage } from "./pages/roles/RolesPage"; import { AppContext } from "./contexts/App"; import { AdminsPage } from "./pages/admins/AdminsPage"; -import WebhooksList from "./containers/webhooks"; -import CreateWebhooks from "./containers/webhooks/create"; -import UpdateWebhooks from "./containers/webhooks/update"; +import { WebhooksPage } from "./pages/webhooks/WebhooksPage"; import AuthLayout from "./layout/auth"; import { OrganizationList } from "./pages/organizations/list"; @@ -104,9 +102,9 @@ export default memo(function AppRoutes() { } /> } /> - }> - } /> - } /> + }> + } /> + } /> } /> diff --git a/web/apps/admin/src/components/CustomField.tsx b/web/lib/admin/components/CustomField.tsx similarity index 96% rename from web/apps/admin/src/components/CustomField.tsx rename to web/lib/admin/components/CustomField.tsx index 1bdab6bbf0..3360412932 100644 --- a/web/apps/admin/src/components/CustomField.tsx +++ b/web/lib/admin/components/CustomField.tsx @@ -9,8 +9,8 @@ import { Flex, Select, Switch, Text, InputField } from "@raystack/apsara"; import React, { CSSProperties } from "react"; import { Control, Controller, UseFormRegister } from "react-hook-form"; -import { capitalizeFirstLetter } from "~/utils/helper"; -import { MultiSelect } from "./multiselect"; +import { capitalizeFirstLetter } from "../utils/helper"; +import { MultiSelect } from "./multiselect/multiselect"; import Skeleton from "react-loading-skeleton"; type CustomFieldNameProps = { @@ -121,7 +121,7 @@ export const CustomFieldName = ({ {...rest} options={options} onSelect={onChange} - selected={value || props?.defaultValue} + selected={value || props?.defaultValue || []} /> ); } diff --git a/web/apps/admin/src/components/sheet/footer.tsx b/web/lib/admin/components/SheetFooter.tsx similarity index 100% rename from web/apps/admin/src/components/sheet/footer.tsx rename to web/lib/admin/components/SheetFooter.tsx diff --git a/web/apps/admin/src/components/multiselect/index.tsx b/web/lib/admin/components/multiselect/multiselect/index.tsx similarity index 100% rename from web/apps/admin/src/components/multiselect/index.tsx rename to web/lib/admin/components/multiselect/multiselect/index.tsx diff --git a/web/lib/admin/index.ts b/web/lib/admin/index.ts index d48a2197e4..e9e4251ba2 100644 --- a/web/lib/admin/index.ts +++ b/web/lib/admin/index.ts @@ -7,6 +7,7 @@ export { ProductsView, ProductPricesView } from "./views/products/exports"; export { default as AuditLogsView } from "./views/audit-logs"; export { default as AdminsView } from "./views/admins"; export { default as PlansView } from "./views/plans"; +export { default as WebhooksView } from "./views/webhooks/webhooks"; // utils exports export { diff --git a/web/lib/admin/utils/helper.ts b/web/lib/admin/utils/helper.ts index 337ee82f3a..dd2c79f820 100644 --- a/web/lib/admin/utils/helper.ts +++ b/web/lib/admin/utils/helper.ts @@ -17,3 +17,7 @@ export function isZeroUUID(uuid: string) { if (typeof uuid !== "string") return false; return uuid.toLowerCase() === ZERO_UUID; } + +export function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/web/apps/admin/src/utils/webhook_events.ts b/web/lib/admin/utils/webhook-events.ts similarity index 100% rename from web/apps/admin/src/utils/webhook_events.ts rename to web/lib/admin/utils/webhook-events.ts diff --git a/web/apps/admin/src/containers/webhooks/columns.tsx b/web/lib/admin/views/webhooks/webhooks/columns.tsx similarity index 98% rename from web/apps/admin/src/containers/webhooks/columns.tsx rename to web/lib/admin/views/webhooks/webhooks/columns.tsx index e4d2fd3ea6..53b28085d8 100644 --- a/web/apps/admin/src/containers/webhooks/columns.tsx +++ b/web/lib/admin/views/webhooks/webhooks/columns.tsx @@ -10,8 +10,8 @@ import { type Webhook } from "@raystack/proton/frontier"; import { timestampToDate, isNullTimestamp, - TimeStamp, -} from "~/utils/connect-timestamp"; + type TimeStamp, +} from "../../../utils/connect-timestamp"; import dayjs from "dayjs"; import { useState } from "react"; import type { useMutation } from "@connectrpc/connect-query"; diff --git a/web/apps/admin/src/containers/webhooks/create/index.tsx b/web/lib/admin/views/webhooks/webhooks/create/index.tsx similarity index 87% rename from web/apps/admin/src/containers/webhooks/create/index.tsx rename to web/lib/admin/views/webhooks/webhooks/create/index.tsx index a20060f849..698ce0c403 100644 --- a/web/apps/admin/src/containers/webhooks/create/index.tsx +++ b/web/lib/admin/views/webhooks/webhooks/create/index.tsx @@ -1,14 +1,14 @@ import { useCallback } from "react"; import { Button, Flex, Sheet } from "@raystack/apsara"; import { useNavigate } from "react-router-dom"; -import { SheetHeader } from "~/components/sheet/header"; +import { SheetHeader } from "../../../../components/SheetHeader"; +import { SheetFooter } from "../../../../components/SheetFooter"; import * as z from "zod"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormSubmit } from "@radix-ui/react-form"; -import { CustomFieldName } from "~/components/CustomField"; -import events from "~/utils/webhook_events"; -import { SheetFooter } from "~/components/sheet/footer"; +import { CustomFieldName } from "../../../../components/CustomField"; +import events from "../../../../utils/webhook-events"; import { toast } from "sonner"; import { useMutation } from "@connectrpc/connect-query"; import { @@ -31,13 +31,18 @@ const NewWebookSchema = z.object({ export type NewWebhook = z.infer; -export default function CreateWebhooks() { +export type CreateWebhooksProps = { + onClose?: () => void; +}; + +export default function CreateWebhooks({ onClose: onCloseProp }: CreateWebhooksProps = {}) { const navigate = useNavigate(); const { invalidateWebhooksList } = useWebhookQueries(); const onOpenChange = useCallback(() => { - navigate("/webhooks"); - }, [navigate]); + if (onCloseProp) onCloseProp(); + else navigate("/webhooks"); + }, [navigate, onCloseProp]); const { mutateAsync: createWebhook, isPending: isSubmitting } = useMutation( AdminServiceQueries.createWebhook, @@ -88,7 +93,7 @@ export default function CreateWebhooks() { ({ label: e, value: e }))} + options={events.map((e: string) => ({ label: e, value: e }))} /> { + +export type WebhooksHeaderProps = { + header?: typeof pageHeader; + onOpenCreate?: () => void; +}; + +export const WebhooksHeader = ({ header = pageHeader, onOpenCreate }: WebhooksHeaderProps) => { const navigate = useNavigate(); + const handleCreate = () => (onOpenCreate ? onOpenCreate() : navigate("/webhooks/create")); return ( { color="neutral" leadingIcon={} data-test-id="admin-create-webhook-btn" - onClick={() => navigate("/webhooks/create")} + onClick={handleCreate} > New Webhook diff --git a/web/apps/admin/src/containers/webhooks/hooks/useWebhookQueries.ts b/web/lib/admin/views/webhooks/webhooks/hooks/useWebhookQueries.ts similarity index 100% rename from web/apps/admin/src/containers/webhooks/hooks/useWebhookQueries.ts rename to web/lib/admin/views/webhooks/webhooks/hooks/useWebhookQueries.ts diff --git a/web/lib/admin/views/webhooks/webhooks/index.tsx b/web/lib/admin/views/webhooks/webhooks/index.tsx new file mode 100644 index 0000000000..db86b8b4ed --- /dev/null +++ b/web/lib/admin/views/webhooks/webhooks/index.tsx @@ -0,0 +1,92 @@ +import { Flex, DataTable, EmptyState } from "@raystack/apsara"; +import { useCallback } from "react"; +import { getColumns } from "./columns"; +import { WebhooksHeader } from "./header"; +import styles from "./webhooks.module.css"; +import { useWebhookQueries } from "./hooks/useWebhookQueries"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import CreateWebhooks from "./create"; +import UpdateWebhooks from "./update"; + +export type WebhooksViewProps = { + selectedWebhookId?: string; + createOpen?: boolean; + onCloseDetail?: () => void; + onSelectWebhook?: (id: string) => void; + onOpenCreate?: () => void; + enableDelete?: boolean; +}; + +export default function WebhooksView({ + selectedWebhookId, + createOpen, + onCloseDetail, + onSelectWebhook, + onOpenCreate, + enableDelete = false, +}: WebhooksViewProps = {}) { + const { + listWebhooks: { + data: webhooksResponse, + isLoading, + error, + isError, + }, + deleteWebhookMutation, + } = useWebhookQueries(); + + const webhooks = webhooksResponse?.webhooks || []; + + const openEditPage = useCallback( + (id: string) => (onSelectWebhook ? onSelectWebhook(id) : undefined), + [onSelectWebhook] + ); + + if (isError) { + console.error("ConnectRPC Error:", error); + return ( + } + heading="Error Loading Webhooks" + subHeading={ + error?.message || + "Something went wrong while loading webhooks. Please try again." + } + /> + ); + } + + const columns = getColumns({ + openEditPage: (id) => { + openEditPage(id); + }, + deleteWebhookMutation, + enableDelete, + }); + + return ( + <> + + + + + + + {createOpen && } + {selectedWebhookId && ( + + )} + + ); +} diff --git a/web/apps/admin/src/containers/webhooks/update/index.tsx b/web/lib/admin/views/webhooks/webhooks/update/index.tsx similarity index 87% rename from web/apps/admin/src/containers/webhooks/update/index.tsx rename to web/lib/admin/views/webhooks/webhooks/update/index.tsx index 49bd1fc2f4..5e4b810c04 100644 --- a/web/apps/admin/src/containers/webhooks/update/index.tsx +++ b/web/lib/admin/views/webhooks/webhooks/update/index.tsx @@ -1,14 +1,14 @@ import { useCallback, useEffect } from "react"; import { Button, Flex, Sheet } from "@raystack/apsara"; import { useNavigate, useParams } from "react-router-dom"; -import { SheetHeader } from "~/components/sheet/header"; +import { SheetHeader } from "../../../../components/SheetHeader"; +import { SheetFooter } from "../../../../components/SheetFooter"; import * as z from "zod"; import { FormProvider, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormSubmit } from "@radix-ui/react-form"; -import { CustomFieldName } from "~/components/CustomField"; -import events from "~/utils/webhook_events"; -import { SheetFooter } from "~/components/sheet/footer"; +import { CustomFieldName } from "../../../../components/CustomField"; +import events from "../../../../utils/webhook-events"; import { toast } from "sonner"; import { useMutation } from "@connectrpc/connect-query"; import { @@ -31,8 +31,16 @@ const UpdateWebhookSchema = z.object({ export type UpdateWebhook = z.infer; -export default function UpdateWebhooks() { +export type UpdateWebhooksProps = { + webhookId?: string; + onClose?: () => void; +}; + +export default function UpdateWebhooks({ webhookId: webhookIdProp, onClose: onCloseProp }: UpdateWebhooksProps = {}) { const navigate = useNavigate(); + const { webhookId: webhookIdParam } = useParams(); + const webhookId = webhookIdProp ?? webhookIdParam ?? ""; + const { listWebhooks: { data: webhooksResponse, @@ -41,11 +49,10 @@ export default function UpdateWebhooks() { invalidateWebhooksList, } = useWebhookQueries(); - const { webhookId = "" } = useParams(); - const onClose = useCallback(() => { - navigate("/webhooks"); - }, [navigate]); + if (onCloseProp) onCloseProp(); + else navigate("/webhooks"); + }, [navigate, onCloseProp]); const methods = useForm({ resolver: zodResolver(UpdateWebhookSchema), @@ -114,7 +121,7 @@ export default function UpdateWebhooks() { ({ label: e, value: e }))} + options={events.map((e: string) => ({ label: e, value: e }))} isLoading={isWebhookLoading} /> [ js: "'use client'" }, format: ['cjs', 'esm'], - external: ['react', 'react-router-dom'], + external: [ + 'react', + 'react-dom', + 'react-router-dom', + 'zod', + '@radix-ui/react-form', + 'sonner', + 'react-loading-skeleton', + '@stitches/react' + ], dts: true, loader: { '.jpg': 'dataurl', diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 577f23d0fe..80f2a689aa 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -276,6 +276,9 @@ importers: '@jest/globals': specifier: ^29.7.0 version: 29.7.0 + '@radix-ui/react-form': + specifier: ^0.0.2 + version: 0.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.2 version: 1.3.2(react@18.3.1) @@ -291,6 +294,9 @@ importers: '@size-limit/preset-small-lib': specifier: ^8.2.6 version: 8.2.6(size-limit@8.2.6) + '@stitches/react': + specifier: ^1.2.8 + version: 1.2.8(react@18.3.1) '@types/jest': specifier: ^29.5.14 version: 29.5.14 @@ -333,6 +339,9 @@ importers: prettier: specifier: ^2.8.8 version: 2.8.8 + react-loading-skeleton: + specifier: ^3.4.0 + version: 3.5.0(react@18.3.1) release-it: specifier: ^16.3.0 version: 16.3.0(typescript@5.9.3) @@ -342,6 +351,9 @@ importers: size-limit: specifier: ^8.2.6 version: 8.2.6 + sonner: + specifier: ^1.4.41 + version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ts-jest: specifier: ^29.3.4 version: 29.4.6(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(esbuild@0.17.19)(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.31)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.15.11)(@types/node@20.19.31)(typescript@5.9.3)))(typescript@5.9.3) @@ -351,6 +363,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.9.3 + zod: + specifier: ^3.22.3 + version: 3.25.76 tools/eslint-config: dependencies: From 32b7eb25e7faf95b785c751ade76f193b16bf758 Mon Sep 17 00:00:00 2001 From: paanSinghCoder Date: Thu, 19 Feb 2026 13:26:19 +0530 Subject: [PATCH 2/5] refactor: update data-testid attributes to data-test-id for consistency across components --- web/lib/admin/components/SheetHeader.tsx | 6 +++--- web/lib/admin/views/roles/index.tsx | 2 +- web/lib/admin/views/webhooks/webhooks/create/index.tsx | 2 +- web/lib/admin/views/webhooks/webhooks/update/index.tsx | 2 +- web/lib/react/components/onboarding/subscribe.tsx | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/web/lib/admin/components/SheetHeader.tsx b/web/lib/admin/components/SheetHeader.tsx index 445472bb01..e011e4f746 100644 --- a/web/lib/admin/components/SheetHeader.tsx +++ b/web/lib/admin/components/SheetHeader.tsx @@ -5,10 +5,10 @@ import { Flex, Text } from "@raystack/apsara"; type SheetHeaderProps = { title: string; onClick: () => void; - "data-testid"?: string; + "data-test-id"?: string; }; -export function SheetHeader({ title, onClick, "data-testid": dataTestId }: SheetHeaderProps) { +export function SheetHeader({ title, onClick, "data-test-id": dataTestId }: SheetHeaderProps) { return ( @@ -17,7 +17,7 @@ export function SheetHeader({ title, onClick, "data-testid": dataTestId }: Sheet ); diff --git a/web/lib/admin/views/roles/index.tsx b/web/lib/admin/views/roles/index.tsx index 3299f71591..855b3cd642 100644 --- a/web/lib/admin/views/roles/index.tsx +++ b/web/lib/admin/views/roles/index.tsx @@ -91,7 +91,7 @@ export default function RolesView({ + + + + + + + ); +}; diff --git a/web/lib/admin/components/CustomField.tsx b/web/sdk/admin/components/CustomField.tsx similarity index 100% rename from web/lib/admin/components/CustomField.tsx rename to web/sdk/admin/components/CustomField.tsx diff --git a/web/lib/admin/components/PageHeader.tsx b/web/sdk/admin/components/PageHeader.tsx similarity index 100% rename from web/lib/admin/components/PageHeader.tsx rename to web/sdk/admin/components/PageHeader.tsx diff --git a/web/lib/admin/components/PageTitle.tsx b/web/sdk/admin/components/PageTitle.tsx similarity index 100% rename from web/lib/admin/components/PageTitle.tsx rename to web/sdk/admin/components/PageTitle.tsx diff --git a/web/lib/admin/components/SheetFooter.tsx b/web/sdk/admin/components/SheetFooter.tsx similarity index 100% rename from web/lib/admin/components/SheetFooter.tsx rename to web/sdk/admin/components/SheetFooter.tsx diff --git a/web/lib/admin/components/SheetHeader.tsx b/web/sdk/admin/components/SheetHeader.tsx similarity index 100% rename from web/lib/admin/components/SheetHeader.tsx rename to web/sdk/admin/components/SheetHeader.tsx diff --git a/web/lib/admin/components/multiselect/multiselect/index.tsx b/web/sdk/admin/components/multiselect/multiselect/index.tsx similarity index 100% rename from web/lib/admin/components/multiselect/multiselect/index.tsx rename to web/sdk/admin/components/multiselect/multiselect/index.tsx diff --git a/web/lib/admin/components/page-header.module.css b/web/sdk/admin/components/page-header.module.css similarity index 100% rename from web/lib/admin/components/page-header.module.css rename to web/sdk/admin/components/page-header.module.css diff --git a/web/sdk/admin/index.ts b/web/sdk/admin/index.ts new file mode 100644 index 0000000000..c0c7b4ece6 --- /dev/null +++ b/web/sdk/admin/index.ts @@ -0,0 +1,35 @@ +"use client"; + +// views exports +export { default as RolesView } from "./views/roles"; +export { default as InvoicesView } from "./views/invoices"; +export { ProductsView, ProductPricesView } from "./views/products/exports"; +export { default as AuditLogsView } from "./views/audit-logs"; +export { default as AdminsView } from "./views/admins"; +export { default as PlansView } from "./views/plans"; +export { default as WebhooksView } from "./views/webhooks/webhooks"; +export { default as PreferencesView } from "./views/preferences/PreferencesView"; +export { default as UsersView } from "./views/users/UsersView"; +export { OrganizationList, type OrganizationListProps } from "./views/organizations/list"; +export { + OrganizationDetails, + type OrganizationDetailsProps, +} from "./views/organizations/details"; +export { OrganizationSecurity } from "./views/organizations/details/security"; +export { OrganizationMembersPage } from "./views/organizations/details/members"; +export { OrganizationProjectssPage } from "./views/organizations/details/projects"; +export { OrganizationInvoicesPage } from "./views/organizations/details/invoices"; +export { OrganizationTokensPage } from "./views/organizations/details/tokens"; +export { OrganizationApisPage } from "./views/organizations/details/apis"; + +// utils exports +export { + getConnectNextPageParam, + DEFAULT_PAGE_SIZE, + getGroupCountMapFromFirstPage, + type ConnectRPCPaginatedResponse, +} from "./utils/connect-pagination"; +export { + transformDataTableQueryToRQLRequest, + type TransformOptions, +} from "./utils/transform-query"; diff --git a/web/lib/admin/utils/connect-pagination.ts b/web/sdk/admin/utils/connect-pagination.ts similarity index 100% rename from web/lib/admin/utils/connect-pagination.ts rename to web/sdk/admin/utils/connect-pagination.ts diff --git a/web/lib/admin/utils/connect-timestamp.ts b/web/sdk/admin/utils/connect-timestamp.ts similarity index 70% rename from web/lib/admin/utils/connect-timestamp.ts rename to web/sdk/admin/utils/connect-timestamp.ts index f08af0d378..aef04cdb53 100644 --- a/web/lib/admin/utils/connect-timestamp.ts +++ b/web/sdk/admin/utils/connect-timestamp.ts @@ -1,10 +1,16 @@ import { timestampDate, type Timestamp } from "@bufbuild/protobuf/wkt"; +import dayjs, { type Dayjs } from "dayjs"; export function timestampToDate(timestamp?: Timestamp): Date | null { if (!timestamp) return null; return timestampDate(timestamp); } +export function timestampToDayjs(timestamp?: Timestamp): Dayjs | null { + const date = timestampToDate(timestamp); + return date ? dayjs(date) : null; +} + /** * Checks if a ConnectRPC Timestamp is the null time (0001-01-01T00:00:00Z) */ diff --git a/web/sdk/admin/utils/constants.ts b/web/sdk/admin/utils/constants.ts new file mode 100644 index 0000000000..46c670d275 --- /dev/null +++ b/web/sdk/admin/utils/constants.ts @@ -0,0 +1,30 @@ +export const SCOPES = { + ORG: "app/organization", + PROJECT: "app/project", + GROUP: "app/group", + USER: "app/user", +} as const; + +export const DEFAULT_ROLES = { + ORG_MANAGER: "app_organization_manager", + ORG_OWNER: "app_organization_owner", + ORG_BILLING_MANAGER: "app_billing_manager", + ORG_VIEWER: "app_organization_viewer", + PROJECT_VIEWER: "app_project_viewer", +} as const; + +export const NULL_DATE = "0001-01-01T00:00:00Z"; + +export interface Config { + title?: string; + app_url?: string; + token_product_id?: string; + organization_types?: string[]; +} + +export const defaultConfig: Config = { + title: "Frontier Admin", + app_url: "example.com", + token_product_id: "token", + organization_types: [], +}; diff --git a/web/lib/admin/utils/helper.ts b/web/sdk/admin/utils/helper.ts similarity index 61% rename from web/lib/admin/utils/helper.ts rename to web/sdk/admin/utils/helper.ts index dd2c79f820..f19d9336db 100644 --- a/web/lib/admin/utils/helper.ts +++ b/web/sdk/admin/utils/helper.ts @@ -21,3 +21,11 @@ export function isZeroUUID(uuid: string) { export function capitalizeFirstLetter(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } + +export function converBillingAddressToString( + address?: { line1?: string; line2?: string; city?: string; state?: string; country?: string; postalCode?: string }, +) { + if (!address) return ""; + const { line1, line2, city, state, country, postalCode } = address; + return [line1, line2, city, state, country, postalCode].filter(Boolean).join(", "); +} diff --git a/web/lib/admin/utils/transform-query.ts b/web/sdk/admin/utils/transform-query.ts similarity index 100% rename from web/lib/admin/utils/transform-query.ts rename to web/sdk/admin/utils/transform-query.ts diff --git a/web/lib/admin/utils/webhook-events.ts b/web/sdk/admin/utils/webhook-events.ts similarity index 100% rename from web/lib/admin/utils/webhook-events.ts rename to web/sdk/admin/utils/webhook-events.ts diff --git a/web/lib/admin/views/admins/admins.module.css b/web/sdk/admin/views/admins/admins.module.css similarity index 100% rename from web/lib/admin/views/admins/admins.module.css rename to web/sdk/admin/views/admins/admins.module.css diff --git a/web/lib/admin/views/admins/columns.tsx b/web/sdk/admin/views/admins/columns.tsx similarity index 100% rename from web/lib/admin/views/admins/columns.tsx rename to web/sdk/admin/views/admins/columns.tsx diff --git a/web/lib/admin/views/admins/index.tsx b/web/sdk/admin/views/admins/index.tsx similarity index 100% rename from web/lib/admin/views/admins/index.tsx rename to web/sdk/admin/views/admins/index.tsx diff --git a/web/lib/admin/views/audit-logs/actor-cell.tsx b/web/sdk/admin/views/audit-logs/actor-cell.tsx similarity index 100% rename from web/lib/admin/views/audit-logs/actor-cell.tsx rename to web/sdk/admin/views/audit-logs/actor-cell.tsx diff --git a/web/lib/admin/views/audit-logs/audit-logs.module.css b/web/sdk/admin/views/audit-logs/audit-logs.module.css similarity index 100% rename from web/lib/admin/views/audit-logs/audit-logs.module.css rename to web/sdk/admin/views/audit-logs/audit-logs.module.css diff --git a/web/lib/admin/views/audit-logs/columns.tsx b/web/sdk/admin/views/audit-logs/columns.tsx similarity index 100% rename from web/lib/admin/views/audit-logs/columns.tsx rename to web/sdk/admin/views/audit-logs/columns.tsx diff --git a/web/lib/admin/views/audit-logs/index.tsx b/web/sdk/admin/views/audit-logs/index.tsx similarity index 100% rename from web/lib/admin/views/audit-logs/index.tsx rename to web/sdk/admin/views/audit-logs/index.tsx diff --git a/web/lib/admin/views/audit-logs/navbar.tsx b/web/sdk/admin/views/audit-logs/navbar.tsx similarity index 100% rename from web/lib/admin/views/audit-logs/navbar.tsx rename to web/sdk/admin/views/audit-logs/navbar.tsx diff --git a/web/lib/admin/views/audit-logs/sidepanel-details.tsx b/web/sdk/admin/views/audit-logs/sidepanel-details.tsx similarity index 100% rename from web/lib/admin/views/audit-logs/sidepanel-details.tsx rename to web/sdk/admin/views/audit-logs/sidepanel-details.tsx diff --git a/web/lib/admin/views/audit-logs/sidepanel-list-id.tsx b/web/sdk/admin/views/audit-logs/sidepanel-list-id.tsx similarity index 100% rename from web/lib/admin/views/audit-logs/sidepanel-list-id.tsx rename to web/sdk/admin/views/audit-logs/sidepanel-list-id.tsx diff --git a/web/lib/admin/views/audit-logs/sidepanel-list-link.tsx b/web/sdk/admin/views/audit-logs/sidepanel-list-link.tsx similarity index 100% rename from web/lib/admin/views/audit-logs/sidepanel-list-link.tsx rename to web/sdk/admin/views/audit-logs/sidepanel-list-link.tsx diff --git a/web/lib/admin/views/audit-logs/sidepanel-log-dialog.tsx b/web/sdk/admin/views/audit-logs/sidepanel-log-dialog.tsx similarity index 100% rename from web/lib/admin/views/audit-logs/sidepanel-log-dialog.tsx rename to web/sdk/admin/views/audit-logs/sidepanel-log-dialog.tsx diff --git a/web/lib/admin/views/audit-logs/util.ts b/web/sdk/admin/views/audit-logs/util.ts similarity index 100% rename from web/lib/admin/views/audit-logs/util.ts rename to web/sdk/admin/views/audit-logs/util.ts diff --git a/web/lib/admin/views/invoices/columns.tsx b/web/sdk/admin/views/invoices/columns.tsx similarity index 100% rename from web/lib/admin/views/invoices/columns.tsx rename to web/sdk/admin/views/invoices/columns.tsx diff --git a/web/lib/admin/views/invoices/index.tsx b/web/sdk/admin/views/invoices/index.tsx similarity index 100% rename from web/lib/admin/views/invoices/index.tsx rename to web/sdk/admin/views/invoices/index.tsx diff --git a/web/lib/admin/views/invoices/invoices.module.css b/web/sdk/admin/views/invoices/invoices.module.css similarity index 100% rename from web/lib/admin/views/invoices/invoices.module.css rename to web/sdk/admin/views/invoices/invoices.module.css diff --git a/web/lib/admin/views/invoices/navbar.tsx b/web/sdk/admin/views/invoices/navbar.tsx similarity index 100% rename from web/lib/admin/views/invoices/navbar.tsx rename to web/sdk/admin/views/invoices/navbar.tsx diff --git a/web/apps/admin/src/pages/organizations/details/apis/apis.module.css b/web/sdk/admin/views/organizations/details/apis/apis.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/apis/apis.module.css rename to web/sdk/admin/views/organizations/details/apis/apis.module.css diff --git a/web/apps/admin/src/pages/organizations/details/apis/columns.tsx b/web/sdk/admin/views/organizations/details/apis/columns.tsx similarity index 96% rename from web/apps/admin/src/pages/organizations/details/apis/columns.tsx rename to web/sdk/admin/views/organizations/details/apis/columns.tsx index 48431adc07..95b39e3550 100644 --- a/web/apps/admin/src/pages/organizations/details/apis/columns.tsx +++ b/web/sdk/admin/views/organizations/details/apis/columns.tsx @@ -1,6 +1,6 @@ import { Text, type DataTableColumnDef } from "@raystack/apsara"; import dayjs from "dayjs"; -import { NULL_DATE } from "~/utils/constants"; +import { NULL_DATE } from "../../../../utils/constants"; import styles from "./apis.module.css"; import type { SearchOrganizationServiceUsersResponse_OrganizationServiceUser, diff --git a/web/apps/admin/src/pages/organizations/details/apis/details-dialog.tsx b/web/sdk/admin/views/organizations/details/apis/details-dialog.tsx similarity index 98% rename from web/apps/admin/src/pages/organizations/details/apis/details-dialog.tsx rename to web/sdk/admin/views/organizations/details/apis/details-dialog.tsx index d3179ef9d7..a490d27312 100644 --- a/web/apps/admin/src/pages/organizations/details/apis/details-dialog.tsx +++ b/web/sdk/admin/views/organizations/details/apis/details-dialog.tsx @@ -10,7 +10,7 @@ import { type SearchOrganizationServiceUsersResponse_OrganizationServiceUser } from "@raystack/proton/frontier"; import { create } from "@bufbuild/protobuf"; -import { timestampToDayjs } from "~/utils/connect-timestamp"; +import { timestampToDayjs } from "../../../../utils/connect-timestamp"; interface ServiceUserDetailsDialogProps { onClose: () => void; diff --git a/web/apps/admin/src/pages/organizations/details/apis/index.tsx b/web/sdk/admin/views/organizations/details/apis/index.tsx similarity index 96% rename from web/apps/admin/src/pages/organizations/details/apis/index.tsx rename to web/sdk/admin/views/organizations/details/apis/index.tsx index c29cf93fea..7426efc89d 100644 --- a/web/apps/admin/src/pages/organizations/details/apis/index.tsx +++ b/web/sdk/admin/views/organizations/details/apis/index.tsx @@ -4,7 +4,7 @@ import styles from "./apis.module.css"; import { InfoCircledIcon, ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { OrganizationContext } from "../contexts/organization-context"; -import PageTitle from "~/components/page-title"; +import { PageTitle } from "../../../../components/PageTitle"; import { getColumns } from "./columns"; import { ServiceUserDetailsDialog } from "./details-dialog"; import { useInfiniteQuery } from "@connectrpc/connect-query"; @@ -13,11 +13,11 @@ import { type SearchOrganizationServiceUsersResponse_OrganizationServiceUser, } from "@raystack/proton/frontier"; import { - transformDataTableQueryToRQLRequest, getConnectNextPageParam, getGroupCountMapFromFirstPage, DEFAULT_PAGE_SIZE, -} from "@raystack/frontier/admin"; +} from "../../../../utils/connect-pagination"; +import { transformDataTableQueryToRQLRequest } from "../../../../utils/transform-query"; import { useDebounceValue } from "usehooks-ts"; const NoCredentials = () => { diff --git a/web/apps/admin/src/pages/organizations/details/contexts/organization-context.tsx b/web/sdk/admin/views/organizations/details/contexts/organization-context.tsx similarity index 89% rename from web/apps/admin/src/pages/organizations/details/contexts/organization-context.tsx rename to web/sdk/admin/views/organizations/details/contexts/organization-context.tsx index 0a5e272425..d4f0854af1 100644 --- a/web/apps/admin/src/pages/organizations/details/contexts/organization-context.tsx +++ b/web/sdk/admin/views/organizations/details/contexts/organization-context.tsx @@ -34,6 +34,10 @@ interface OrganizationContextType { updateKYCDetails: (kycDetails: OrganizationKyc | undefined) => void; kycDetails?: OrganizationKyc; isKYCLoading: boolean; + appUrl?: string; + tokenProductId?: string; + countries?: string[]; + organizationTypes?: string[]; } const defaultOrganiztionContextValue = { @@ -56,6 +60,10 @@ const defaultOrganiztionContextValue = { updateKYCDetails: () => {}, kycDetails: undefined, isKYCLoading: false, + appUrl: undefined, + tokenProductId: undefined, + countries: undefined, + organizationTypes: undefined, }; export const OrganizationContext = createContext(defaultOrganiztionContextValue); diff --git a/web/apps/admin/src/pages/organizations/details/edit/billing.tsx b/web/sdk/admin/views/organizations/details/edit/billing.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/edit/billing.tsx rename to web/sdk/admin/views/organizations/details/edit/billing.tsx diff --git a/web/apps/admin/src/pages/organizations/details/edit/edit.module.css b/web/sdk/admin/views/organizations/details/edit/edit.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/edit/edit.module.css rename to web/sdk/admin/views/organizations/details/edit/edit.module.css diff --git a/web/apps/admin/src/pages/organizations/details/edit/kyc.tsx b/web/sdk/admin/views/organizations/details/edit/kyc.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/edit/kyc.tsx rename to web/sdk/admin/views/organizations/details/edit/kyc.tsx diff --git a/web/apps/admin/src/pages/organizations/details/edit/organization.tsx b/web/sdk/admin/views/organizations/details/edit/organization.tsx similarity index 94% rename from web/apps/admin/src/pages/organizations/details/edit/organization.tsx rename to web/sdk/admin/views/organizations/details/edit/organization.tsx index f219642605..fbfedf1933 100644 --- a/web/apps/admin/src/pages/organizations/details/edit/organization.tsx +++ b/web/sdk/admin/views/organizations/details/edit/organization.tsx @@ -13,8 +13,7 @@ import { Label, } from "@raystack/apsara"; import { Cross1Icon } from "@radix-ui/react-icons"; -import { AvatarUpload } from "@raystack/frontier/react"; -import { AppContext } from "~/contexts/App"; +import { AvatarUpload } from "../../../../../react/components/avatar-upload"; import { z } from "zod"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -44,11 +43,6 @@ const orgUpdateSchema = z type OrgUpdateSchema = z.infer; -async function loadCountries() { - const data = await import("~/assets/data/countries.json"); - return data.default; -} - interface MetaData extends Record { size?: number; type?: string; @@ -83,20 +77,15 @@ function getDefaultValue(organization: Organization, industries: string[]) { } export function EditOrganizationPanel({ onClose }: { onClose: () => void }) { - const { config } = useContext(AppContext); - const { organization } = useContext(OrganizationContext); - const [countries, setCountries] = useState([]); + const { organization, appUrl, countries: countriesFromContext = [], organizationTypes: industries = [] } = useContext(OrganizationContext); + const [countries, setCountries] = useState(countriesFromContext); const queryClient = useQueryClient(); const transport = useTransport(); - - const industries = config?.organization_types || []; const orgId = organization?.id || ""; useEffect(() => { - loadCountries().then((list) => - setCountries(list.map((country) => country.name)), - ); - }, []); + if (countriesFromContext.length > 0) setCountries(countriesFromContext); + }, [countriesFromContext]); const { handleSubmit, @@ -216,7 +205,7 @@ export function EditOrganizationPanel({ onClose }: { onClose: () => void }) { Promise; + onExportProjects?: () => Promise; + onExportTokens?: () => Promise; + appUrl?: string; + tokenProductId?: string; + countries?: string[]; + organizationTypes?: string[]; +}; + +export const OrganizationDetails = ({ + organizationId, + onExportMembers, + onExportProjects, + onExportTokens, + appUrl, + tokenProductId, + countries, + organizationTypes, +}: OrganizationDetailsProps) => { + const [isSearchVisible, setIsSearchVisible] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const queryClient = useQueryClient(); + const transport = useTransport(); + + // Use Connect RPC for fetching organization + const { + data: organization, + isLoading: isOrganizationLoading, + error: organizationError, + } = useQuery( + FrontierServiceQueries.getOrganization, + { id: organizationId }, + { + enabled: !!organizationId, + select: (data) => data?.organization, + }, + ); + + const getOrganizationQueryKey = [ + FrontierServiceQueries.getOrganization, + { id: organizationId }, + ]; + + async function updateOrganization(org: Organization) { + queryClient.setQueryData(getOrganizationQueryKey, { organization: org }); + } + + // Fetch KYC details + const { + data: kycData, + isLoading: isKYCLoading, + error: kycError, + } = useQuery( + FrontierServiceQueries.getOrganizationKyc, + { orgId: organizationId || "" }, + { + enabled: !!organizationId, + }, + ); + + const kycDetails = useMemo(() => kycData?.organizationKyc, [kycData]); + + function updateKYCDetails(kyc: typeof kycDetails) { + if (!organizationId) return; + queryClient.setQueryData( + createConnectQueryKey({ + schema: FrontierServiceQueries.getOrganizationKyc, + transport, + input: { orgId: organizationId }, + cardinality: "finite", + }), + create(GetOrganizationKycResponseSchema, { organizationKyc: kyc }), + ); + } + + // 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, + ); + }, + }, + ); + + // Fetch billing accounts list + const { data: firstBillingAccountId = "", error: billingAccountsError } = + useQuery( + FrontierServiceQueries.listBillingAccounts, + { orgId: organizationId || "" }, + { + enabled: !!organizationId, + select: (data) => data?.billingAccounts?.[0]?.id || "", + }, + ); + + // 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 isLoading = + isOrganizationLoading || + isDefaultRolesLoading || + isOrgRolesLoading || + isBillingAccountLoading; + return ( + + + {organization?.id ? ( + + ) : null} + + + ); +}; diff --git a/web/apps/admin/src/pages/organizations/details/invoices/columns.tsx b/web/sdk/admin/views/organizations/details/invoices/columns.tsx similarity index 95% rename from web/apps/admin/src/pages/organizations/details/invoices/columns.tsx rename to web/sdk/admin/views/organizations/details/invoices/columns.tsx index 36453ffbf1..10f4e15ac9 100644 --- a/web/apps/admin/src/pages/organizations/details/invoices/columns.tsx +++ b/web/sdk/admin/views/organizations/details/invoices/columns.tsx @@ -1,4 +1,4 @@ -import { NULL_DATE } from "~/utils/constants"; +import { NULL_DATE } from "../../../../utils/constants"; import styles from "./invoices.module.css"; import dayjs from "dayjs"; import { DataTableColumnDef, Link, Amount } from "@raystack/apsara"; @@ -9,7 +9,7 @@ import { isNullTimestamp, TimeStamp, timestampToDate, -} from "~/utils/connect-timestamp"; +} from "../../../../utils/connect-timestamp"; // https://docs.stripe.com/invoicing/overview#invoice-statuses const InvoiceStatusesMap = { diff --git a/web/apps/admin/src/pages/organizations/details/invoices/index.tsx b/web/sdk/admin/views/organizations/details/invoices/index.tsx similarity index 96% rename from web/apps/admin/src/pages/organizations/details/invoices/index.tsx rename to web/sdk/admin/views/organizations/details/invoices/index.tsx index a251d251a3..bbe73dba76 100644 --- a/web/apps/admin/src/pages/organizations/details/invoices/index.tsx +++ b/web/sdk/admin/views/organizations/details/invoices/index.tsx @@ -4,16 +4,16 @@ import styles from "./invoices.module.css"; import { FileTextIcon, ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useContext, useEffect, useMemo, useState } from "react"; import { OrganizationContext } from "../contexts/organization-context"; -import PageTitle from "~/components/page-title"; +import { PageTitle } from "../../../../components/PageTitle"; import { getColumns } from "./columns"; import { AdminServiceQueries } from "@raystack/proton/frontier"; import { useInfiniteQuery } from "@connectrpc/connect-query"; import { - transformDataTableQueryToRQLRequest, getConnectNextPageParam, DEFAULT_PAGE_SIZE, getGroupCountMapFromFirstPage, -} from "@raystack/frontier/admin"; +} from "../../../../utils/connect-pagination"; +import { transformDataTableQueryToRQLRequest } from "../../../../utils/transform-query"; import { useDebounceValue } from "usehooks-ts"; const DEFAULT_SORT: DataTableSort = { name: "created_at", order: "desc" }; diff --git a/web/apps/admin/src/pages/organizations/details/invoices/invoices.module.css b/web/sdk/admin/views/organizations/details/invoices/invoices.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/invoices/invoices.module.css rename to web/sdk/admin/views/organizations/details/invoices/invoices.module.css diff --git a/web/apps/admin/src/pages/organizations/details/layout/add-tokens-dialog.tsx b/web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx similarity index 94% rename from web/apps/admin/src/pages/organizations/details/layout/add-tokens-dialog.tsx rename to web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx index 85ae6787bc..9ff9c1e34e 100644 --- a/web/apps/admin/src/pages/organizations/details/layout/add-tokens-dialog.tsx +++ b/web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx @@ -13,8 +13,7 @@ import { Controller, FormProvider, useForm } from "react-hook-form"; import styles from "./layout.module.css"; import * as z from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; -import { AppContext } from "~/contexts/App"; -import { defaultConfig } from "~/utils/constants"; +import { defaultConfig } from "../../../../utils/constants"; import { useMutation, createConnectQueryKey, useTransport } from "@connectrpc/connect-query"; import { useQueryClient } from "@tanstack/react-query"; import { AdminServiceQueries, CheckoutProductBodySchema, DelegatedCheckoutRequestSchema } from "@raystack/proton/frontier"; @@ -38,8 +37,7 @@ const addTokensSchema = z.object({ type AddTokenRequestType = z.infer; export const AddTokensDialog = ({ onOpenChange }: InviteUsersDialogProps) => { - const { config } = useContext(AppContext); - const { organization, billingAccount, fetchTokenBalance } = + const { organization, billingAccount, fetchTokenBalance, tokenProductId } = useContext(OrganizationContext); const queryClient = useQueryClient(); const transport = useTransport(); @@ -50,7 +48,7 @@ export const AddTokensDialog = ({ onOpenChange }: InviteUsersDialogProps) => { resolver: zodResolver(addTokensSchema), defaultValues: { quantity: "0", - product: config?.token_product_id || defaultConfig.token_product_id, + product: tokenProductId ?? defaultConfig.token_product_id ?? "token", }, }); diff --git a/web/apps/admin/src/pages/organizations/details/layout/index.tsx b/web/sdk/admin/views/organizations/details/layout/index.tsx similarity index 84% rename from web/apps/admin/src/pages/organizations/details/layout/index.tsx rename to web/sdk/admin/views/organizations/details/layout/index.tsx index 73e5b0e686..fc7482be7b 100644 --- a/web/apps/admin/src/pages/organizations/details/layout/index.tsx +++ b/web/sdk/admin/views/organizations/details/layout/index.tsx @@ -1,11 +1,10 @@ import { OrganizationsDetailsNavabar } from "./navbar"; import styles from "./layout.module.css"; -import { EmptyState, Flex } from "@raystack/apsara"; +import { EmptyState, Flex, Spinner } from "@raystack/apsara"; import { OrgSidePanel } from "../side-panel/"; import React, { useState } from "react"; -import LoadingState from "~/components/states/Loading"; import { OrganizationIcon } from "@raystack/apsara/icons"; -import PageTitle from "~/components/page-title"; +import { PageTitle } from "../../../../components/PageTitle"; import { EditKYCPanel } from "../edit/kyc"; import { EditOrganizationPanel } from "../edit/organization"; import { EditBillingPanel } from "../edit/billing"; @@ -15,12 +14,18 @@ interface OrganizationDetailsLayoutProps { isLoading: boolean; organization?: Organization; children: React.ReactNode; + onExportMembers?: () => Promise; + onExportProjects?: () => Promise; + onExportTokens?: () => Promise; } export const OrganizationDetailsLayout = ({ isLoading, organization, children, + onExportMembers, + onExportProjects, + onExportTokens, }: OrganizationDetailsLayoutProps) => { const [showSidePanel, setShowSidePanel] = useState(true); const [showKYCPanel, setShowKYCPanel] = useState(false); @@ -58,8 +63,9 @@ export const OrganizationDetailsLayout = ({ const title = `${organization?.title} | Organizations`; return isLoading ? ( - // TODO: make better loading state for page - + + + ) : organization ? ( @@ -69,6 +75,9 @@ export const OrganizationDetailsLayout = ({ openKYCPanel={openKYCPanel} openEditOrgPanel={openEditOrgPanel} openEditBillingPanel={openEditBillingPanel} + onExportMembers={onExportMembers} + onExportProjects={onExportProjects} + onExportTokens={onExportTokens} /> void; openEditOrgPanel: () => void; openEditBillingPanel: () => void; + onExportMembers?: () => Promise; + onExportProjects?: () => Promise; + onExportTokens?: () => Promise; } const NavbarActionMenu = ({ @@ -70,6 +63,9 @@ const NavbarActionMenu = ({ openEditOrgPanel, openEditBillingPanel, organizationTitle, + onExportMembers, + onExportProjects, + onExportTokens, }: NavbarActionMenuProps) => { const [isInviteUsersDialogOpen, setIsInviteUsersDialogOpen] = useState(false); const [isAddTokensDialogOpen, setIsAddTokensDialogOpen] = useState(false); @@ -87,13 +83,10 @@ const NavbarActionMenu = ({ async function handleExportMembers(e: React.MouseEvent) { e.preventDefault(); + if (!onExportMembers) return; try { setIsMembersDownloading(true); - await exportCsvFromStream( - adminClient.exportOrganizationUsers, - create(ExportOrganizationUsersRequestSchema, { id: organizationId }), - `${organizationTitle}_members.csv`, - ); + await onExportMembers(); } catch (error) { toast.error("Failed to export members"); console.error(error); @@ -104,13 +97,10 @@ const NavbarActionMenu = ({ async function handleExportProjects(e: React.MouseEvent) { e.preventDefault(); + if (!onExportProjects) return; try { setIsProjectsDownloading(true); - await exportCsvFromStream( - adminClient.exportOrganizationProjects, - create(ExportOrganizationProjectsRequestSchema, { id: organizationId }), - `${organizationTitle}_projects.csv`, - ); + await onExportProjects(); } catch (error) { toast.error("Failed to export projects"); console.error(error); @@ -121,13 +111,10 @@ const NavbarActionMenu = ({ async function handleExportTokens(e: React.MouseEvent) { e.preventDefault(); + if (!onExportTokens) return; try { setIsTokensDownloading(true); - await exportCsvFromStream( - adminClient.exportOrganizationTokens, - create(ExportOrganizationTokensRequestSchema, { id: organizationId }), - `${organizationTitle}_tokens.csv`, - ); + await onExportTokens(); } catch (error) { toast.error("Failed to export tokens"); console.error(error); @@ -136,22 +123,36 @@ const NavbarActionMenu = ({ } } + const exportSubItems: navConfig[] = []; + if (onExportMembers) { + exportSubItems.push({ + label: "Members", + onClick: handleExportMembers, + isLoading: isMembersDownloading, + }); + } + if (onExportProjects) { + exportSubItems.push({ + label: "Projects", + onClick: handleExportProjects, + isLoading: isProjectsDownloading, + }); + } + if (onExportTokens) { + exportSubItems.push({ + label: "Tokens", + onClick: handleExportTokens, + isLoading: isTokensDownloading, + }); + } + const items: navConfig[] = [ { label: "Edit...", subItems: [ - { - label: "Organization...", - onClick: openEditOrgPanel, - }, - { - label: "Billing...", - onClick: openEditBillingPanel, - }, - { - label: "KYC...", - onClick: openKYCPanel, - }, + { label: "Organization...", onClick: openEditOrgPanel }, + { label: "Billing...", onClick: openEditBillingPanel }, + { label: "KYC...", onClick: openKYCPanel }, ], }, { @@ -170,26 +171,9 @@ const NavbarActionMenu = ({ label: "Change plan...", disabled: true, }, - { - label: "Export", - subItems: [ - { - label: "Members", - onClick: handleExportMembers, - isLoading: isMembersDownloading, - }, - { - label: "Projects", - onClick: handleExportProjects, - isLoading: isProjectsDownloading, - }, - { - label: "Tokens", - onClick: handleExportTokens, - isLoading: isTokensDownloading, - }, - ], - }, + ...(exportSubItems.length > 0 + ? [{ label: "Export", subItems: exportSubItems }] + : []), ]; return ( @@ -278,6 +262,9 @@ interface OrganizationDetailsNavbarProps { openKYCPanel: () => void; openEditOrgPanel: () => void; openEditBillingPanel: () => void; + onExportMembers?: () => Promise; + onExportProjects?: () => Promise; + onExportTokens?: () => Promise; } export const OrganizationsDetailsNavabar = ({ @@ -286,6 +273,9 @@ export const OrganizationsDetailsNavabar = ({ openKYCPanel, openEditBillingPanel, openEditOrgPanel, + onExportMembers, + onExportProjects, + onExportTokens, }: OrganizationDetailsNavbarProps) => { const { search } = useContext(OrganizationContext); @@ -325,14 +315,18 @@ export const OrganizationsDetailsNavabar = ({ openEditOrgPanel={openEditOrgPanel} openEditBillingPanel={openEditBillingPanel} organizationTitle={organization?.title || ""} + onExportMembers={onExportMembers} + onExportProjects={onExportProjects} + onExportTokens={onExportTokens} /> {search.isVisible ? ( - ) : null} diff --git a/web/apps/admin/src/pages/organizations/details/members/columns.tsx b/web/sdk/admin/views/organizations/details/members/columns.tsx similarity index 98% rename from web/apps/admin/src/pages/organizations/details/members/columns.tsx rename to web/sdk/admin/views/organizations/details/members/columns.tsx index 66ec0c6b17..a102f1926b 100644 --- a/web/apps/admin/src/pages/organizations/details/members/columns.tsx +++ b/web/sdk/admin/views/organizations/details/members/columns.tsx @@ -17,7 +17,7 @@ import { isNullTimestamp, TimeStamp, timestampToDate, -} from "~/utils/connect-timestamp"; +} from "../../../../utils/connect-timestamp"; const MemberStates = { enabled: "Active", diff --git a/web/apps/admin/src/pages/organizations/details/members/index.tsx b/web/sdk/admin/views/organizations/details/members/index.tsx similarity index 94% rename from web/apps/admin/src/pages/organizations/details/members/index.tsx rename to web/sdk/admin/views/organizations/details/members/index.tsx index c9c6f677fd..f3681cbf76 100644 --- a/web/apps/admin/src/pages/organizations/details/members/index.tsx +++ b/web/sdk/admin/views/organizations/details/members/index.tsx @@ -1,6 +1,6 @@ import { DataTable, EmptyState, Flex } from "@raystack/apsara"; import type { DataTableQuery, DataTableSort } from "@raystack/apsara"; -import PageTitle from "~/components/page-title"; +import { PageTitle } from "../../../../components/PageTitle"; import styles from "./members.module.css"; import { useContext, useEffect, useMemo, useState } from "react"; import { getColumns } from "./columns"; @@ -12,16 +12,13 @@ import { useTransport, } from "@connectrpc/connect-query"; import { useQueryClient } from "@tanstack/react-query"; -import UserIcon from "~/assets/icons/users.svg?react"; +import { UsersIcon } from "../../../../assets/icons/UsersIcon"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { OrganizationContext } from "../contexts/organization-context"; -import { AssignRole } from "~/components/assign-role"; +import { AssignRole } from "../../../../components/AssignRole"; import { RemoveMember } from "./remove-member"; -import { - transformDataTableQueryToRQLRequest, - getConnectNextPageParam, - DEFAULT_PAGE_SIZE, -} from "@raystack/frontier/admin"; +import { getConnectNextPageParam, DEFAULT_PAGE_SIZE } from "../../../../utils/connect-pagination"; +import { transformDataTableQueryToRQLRequest } from "../../../../utils/transform-query"; import { useDebounceValue } from "usehooks-ts"; const DEFAULT_SORT: DataTableSort = { name: "org_joined_at", order: "desc" }; @@ -47,7 +44,7 @@ const NoMembers = () => { }} heading="No Member found" subHeading="We couldn't find any matches for that keyword or filter. Try alternative terms or check for typos." - icon={} + icon={} /> ); }; diff --git a/web/apps/admin/src/pages/organizations/details/members/members.module.css b/web/sdk/admin/views/organizations/details/members/members.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/members/members.module.css rename to web/sdk/admin/views/organizations/details/members/members.module.css diff --git a/web/apps/admin/src/pages/organizations/details/members/remove-member.tsx b/web/sdk/admin/views/organizations/details/members/remove-member.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/members/remove-member.tsx rename to web/sdk/admin/views/organizations/details/members/remove-member.tsx diff --git a/web/apps/admin/src/pages/organizations/details/projects/columns.tsx b/web/sdk/admin/views/organizations/details/projects/columns.tsx similarity index 99% rename from web/apps/admin/src/pages/organizations/details/projects/columns.tsx rename to web/sdk/admin/views/organizations/details/projects/columns.tsx index 1c3cbd95ca..c8460c6471 100644 --- a/web/apps/admin/src/pages/organizations/details/projects/columns.tsx +++ b/web/sdk/admin/views/organizations/details/projects/columns.tsx @@ -18,7 +18,7 @@ import { isNullTimestamp, TimeStamp, timestampToDate, -} from "~/utils/connect-timestamp"; +} from "../../../../utils/connect-timestamp"; import { DotsHorizontalIcon } from "@radix-ui/react-icons"; import { RenameProjectDialog } from "./rename-project"; import { useState } from "react"; diff --git a/web/apps/admin/src/pages/organizations/details/projects/index.tsx b/web/sdk/admin/views/organizations/details/projects/index.tsx similarity index 95% rename from web/apps/admin/src/pages/organizations/details/projects/index.tsx rename to web/sdk/admin/views/organizations/details/projects/index.tsx index ed23f0744e..5220164f8a 100644 --- a/web/apps/admin/src/pages/organizations/details/projects/index.tsx +++ b/web/sdk/admin/views/organizations/details/projects/index.tsx @@ -5,7 +5,7 @@ import { type DataTableQuery, type DataTableSort, } from "@raystack/apsara"; -import PageTitle from "~/components/page-title"; +import { PageTitle } from "../../../../components/PageTitle"; import styles from "./projects.module.css"; import { useContext, useEffect, useMemo, useState } from "react"; import { getColumns } from "./columns"; @@ -20,11 +20,8 @@ import { useQueryClient } from "@tanstack/react-query"; import { OrganizationContext } from "../contexts/organization-context"; import { FileIcon, ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { ProjectMembersDialog } from "./members"; -import { - transformDataTableQueryToRQLRequest, - getConnectNextPageParam, - DEFAULT_PAGE_SIZE, -} from "@raystack/frontier/admin"; +import { getConnectNextPageParam, DEFAULT_PAGE_SIZE } from "../../../../utils/connect-pagination"; +import { transformDataTableQueryToRQLRequest } from "../../../../utils/transform-query"; import { useDebounceValue } from "usehooks-ts"; const DEFAULT_SORT: DataTableSort = { name: "created_at", order: "desc" }; diff --git a/web/apps/admin/src/pages/organizations/details/projects/members/add-members-dropdown.tsx b/web/sdk/admin/views/organizations/details/projects/members/add-members-dropdown.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/projects/members/add-members-dropdown.tsx rename to web/sdk/admin/views/organizations/details/projects/members/add-members-dropdown.tsx diff --git a/web/apps/admin/src/pages/organizations/details/projects/members/assign-role.tsx b/web/sdk/admin/views/organizations/details/projects/members/assign-role.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/projects/members/assign-role.tsx rename to web/sdk/admin/views/organizations/details/projects/members/assign-role.tsx diff --git a/web/apps/admin/src/pages/organizations/details/projects/members/columns.tsx b/web/sdk/admin/views/organizations/details/projects/members/columns.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/projects/members/columns.tsx rename to web/sdk/admin/views/organizations/details/projects/members/columns.tsx diff --git a/web/apps/admin/src/pages/organizations/details/projects/members/index.tsx b/web/sdk/admin/views/organizations/details/projects/members/index.tsx similarity index 96% rename from web/apps/admin/src/pages/organizations/details/projects/members/index.tsx rename to web/sdk/admin/views/organizations/details/projects/members/index.tsx index 6b8d95f5a1..c2ed1f0091 100644 --- a/web/apps/admin/src/pages/organizations/details/projects/members/index.tsx +++ b/web/sdk/admin/views/organizations/details/projects/members/index.tsx @@ -14,17 +14,14 @@ import { create } from "@bufbuild/protobuf"; import { useQuery, useInfiniteQuery } from "@connectrpc/connect-query"; import { useDebouncedState } from "@raystack/apsara/hooks"; import styles from "./members.module.css"; -import UserIcon from "~/assets/icons/users.svg?react"; +import { UsersIcon } from "../../../../../assets/icons/UsersIcon"; import { getColumns } from "./columns"; import { AssignRole } from "./assign-role"; import { PROJECT_NAMESPACE } from "../../types"; import { RemoveMember } from "./remove-member"; import { AddMembersDropdown } from "./add-members-dropdown"; -import { - getConnectNextPageParam, - DEFAULT_PAGE_SIZE, - transformDataTableQueryToRQLRequest, -} from "@raystack/frontier/admin"; +import { getConnectNextPageParam, DEFAULT_PAGE_SIZE } from "../../../../../utils/connect-pagination"; +import { transformDataTableQueryToRQLRequest } from "../../../../../utils/transform-query"; const NoMembers = () => { return ( @@ -35,7 +32,7 @@ const NoMembers = () => { }} heading="No Members found" subHeading="We couldn’t find any matches for that keyword or filter. Try alternative terms or check for typos." - icon={} + icon={} /> ); }; diff --git a/web/apps/admin/src/pages/organizations/details/projects/members/members.module.css b/web/sdk/admin/views/organizations/details/projects/members/members.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/projects/members/members.module.css rename to web/sdk/admin/views/organizations/details/projects/members/members.module.css diff --git a/web/apps/admin/src/pages/organizations/details/projects/members/remove-member.tsx b/web/sdk/admin/views/organizations/details/projects/members/remove-member.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/projects/members/remove-member.tsx rename to web/sdk/admin/views/organizations/details/projects/members/remove-member.tsx diff --git a/web/apps/admin/src/pages/organizations/details/projects/projects.module.css b/web/sdk/admin/views/organizations/details/projects/projects.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/projects/projects.module.css rename to web/sdk/admin/views/organizations/details/projects/projects.module.css diff --git a/web/apps/admin/src/pages/organizations/details/projects/rename-project.tsx b/web/sdk/admin/views/organizations/details/projects/rename-project.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/projects/rename-project.tsx rename to web/sdk/admin/views/organizations/details/projects/rename-project.tsx diff --git a/web/apps/admin/src/pages/organizations/details/projects/use-add-project-members.tsx b/web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx similarity index 97% rename from web/apps/admin/src/pages/organizations/details/projects/use-add-project-members.tsx rename to web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx index eb3dee62d8..5df76a6702 100644 --- a/web/apps/admin/src/pages/organizations/details/projects/use-add-project-members.tsx +++ b/web/sdk/admin/views/organizations/details/projects/use-add-project-members.tsx @@ -1,7 +1,7 @@ import { useCallback, useContext, useMemo, useState } from "react"; import { OrganizationContext } from "../contexts/organization-context"; import { toast } from "@raystack/apsara"; -import { DEFAULT_ROLES } from "~/utils/constants"; +import { DEFAULT_ROLES } from "../../../../utils/constants"; import { useQuery, useMutation } from "@connectrpc/connect-query"; import { FrontierServiceQueries, ListProjectUsersRequestSchema, CreatePolicyRequestSchema } from "@raystack/proton/frontier"; import { create } from "@bufbuild/protobuf"; diff --git a/web/apps/admin/src/pages/organizations/details/security/block-organization.tsx b/web/sdk/admin/views/organizations/details/security/block-organization.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/security/block-organization.tsx rename to web/sdk/admin/views/organizations/details/security/block-organization.tsx diff --git a/web/apps/admin/src/pages/organizations/details/security/domains-list.tsx b/web/sdk/admin/views/organizations/details/security/domains-list.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/security/domains-list.tsx rename to web/sdk/admin/views/organizations/details/security/domains-list.tsx diff --git a/web/apps/admin/src/pages/organizations/details/security/index.tsx b/web/sdk/admin/views/organizations/details/security/index.tsx similarity index 97% rename from web/apps/admin/src/pages/organizations/details/security/index.tsx rename to web/sdk/admin/views/organizations/details/security/index.tsx index b333e9a4ce..f9ca9dd485 100644 --- a/web/apps/admin/src/pages/organizations/details/security/index.tsx +++ b/web/sdk/admin/views/organizations/details/security/index.tsx @@ -14,7 +14,7 @@ import { useOutletContext } from "react-router-dom"; import { OutletContext } from "../types"; import { BlockOrganizationSection } from "./block-organization"; import { DomainsList } from "./domains-list"; -import PageTitle from "~/components/page-title"; +import { PageTitle } from "../../../../components/PageTitle"; import { useQuery } from "@connectrpc/connect-query"; import { FrontierServiceQueries, ListOrganizationDomainsRequestSchema } from "@raystack/proton/frontier"; import { create } from "@bufbuild/protobuf"; diff --git a/web/apps/admin/src/pages/organizations/details/security/security.module.css b/web/sdk/admin/views/organizations/details/security/security.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/security/security.module.css rename to web/sdk/admin/views/organizations/details/security/security.module.css diff --git a/web/apps/admin/src/pages/organizations/details/side-panel/billing-details-section.tsx b/web/sdk/admin/views/organizations/details/side-panel/billing-details-section.tsx similarity index 97% rename from web/apps/admin/src/pages/organizations/details/side-panel/billing-details-section.tsx rename to web/sdk/admin/views/organizations/details/side-panel/billing-details-section.tsx index cb3b488c66..424b250ebd 100644 --- a/web/apps/admin/src/pages/organizations/details/side-panel/billing-details-section.tsx +++ b/web/sdk/admin/views/organizations/details/side-panel/billing-details-section.tsx @@ -1,6 +1,6 @@ import { CopyButton, Flex, Link, List, Text } from "@raystack/apsara"; import styles from "./side-panel.module.css"; -import { converBillingAddressToString } from "~/utils/helper"; +import { converBillingAddressToString } from "../../../../utils/helper"; import Skeleton from "react-loading-skeleton"; import { useContext, useEffect } from "react"; import { CalendarIcon } from "@radix-ui/react-icons"; @@ -9,7 +9,7 @@ import { OrganizationContext } from "../contexts/organization-context"; import { useQuery } from "@connectrpc/connect-query"; import { FrontierServiceQueries, GetUpcomingInvoiceRequestSchema } from "@raystack/proton/frontier"; import { create } from "@bufbuild/protobuf"; -import { timestampToDayjs } from "~/utils/connect-timestamp"; +import { timestampToDayjs } from "../../../../utils/connect-timestamp"; export const BillingDetailsSection = () => { const { billingAccount, organization } = useContext(OrganizationContext); diff --git a/web/apps/admin/src/pages/organizations/details/side-panel/index.tsx b/web/sdk/admin/views/organizations/details/side-panel/index.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/side-panel/index.tsx rename to web/sdk/admin/views/organizations/details/side-panel/index.tsx diff --git a/web/apps/admin/src/pages/organizations/details/side-panel/kyc-section.tsx b/web/sdk/admin/views/organizations/details/side-panel/kyc-section.tsx similarity index 100% rename from web/apps/admin/src/pages/organizations/details/side-panel/kyc-section.tsx rename to web/sdk/admin/views/organizations/details/side-panel/kyc-section.tsx diff --git a/web/apps/admin/src/pages/organizations/details/side-panel/org-details-section.tsx b/web/sdk/admin/views/organizations/details/side-panel/org-details-section.tsx similarity index 92% rename from web/apps/admin/src/pages/organizations/details/side-panel/org-details-section.tsx rename to web/sdk/admin/views/organizations/details/side-panel/org-details-section.tsx index a7571f60d5..d100edee6d 100644 --- a/web/apps/admin/src/pages/organizations/details/side-panel/org-details-section.tsx +++ b/web/sdk/admin/views/organizations/details/side-panel/org-details-section.tsx @@ -3,9 +3,9 @@ import { Flex, List, Text, CopyButton, Tooltip } from "@raystack/apsara"; import styles from "./side-panel.module.css"; import dayjs from "dayjs"; import { useContext } from "react"; -import { AppContext } from "~/contexts/App"; +import { OrganizationContext } from "../contexts/organization-context"; import type { Organization } from "@raystack/proton/frontier"; -import { timestampToDate } from "~/utils/connect-timestamp"; +import { timestampToDate } from "../../../../utils/connect-timestamp"; interface OrganizationDetailsSectionProps { organization: Organization; @@ -16,7 +16,7 @@ type Metadata = Record; export const OrganizationDetailsSection = ({ organization, }: OrganizationDetailsSectionProps) => { - const { config } = useContext(AppContext); + const { appUrl } = useContext(OrganizationContext); return ( @@ -27,7 +27,7 @@ export const OrganizationDetailsSection = ({ - {config?.app_url}/{organization.name} + {appUrl ? `${appUrl}/${organization.name}` : organization.name} diff --git a/web/apps/admin/src/pages/organizations/details/side-panel/plan-details-section.tsx b/web/sdk/admin/views/organizations/details/side-panel/plan-details-section.tsx similarity index 97% rename from web/apps/admin/src/pages/organizations/details/side-panel/plan-details-section.tsx rename to web/sdk/admin/views/organizations/details/side-panel/plan-details-section.tsx index 9afb543d8a..7a570f75de 100644 --- a/web/apps/admin/src/pages/organizations/details/side-panel/plan-details-section.tsx +++ b/web/sdk/admin/views/organizations/details/side-panel/plan-details-section.tsx @@ -7,7 +7,7 @@ import { OrganizationContext } from "../contexts/organization-context"; import { useQuery } from "@connectrpc/connect-query"; import { FrontierServiceQueries, ListSubscriptionsRequestSchema, GetPlanRequestSchema } from "@raystack/proton/frontier"; import { create } from "@bufbuild/protobuf"; -import { timestampToDayjs } from "~/utils/connect-timestamp"; +import { timestampToDayjs } from "../../../../utils/connect-timestamp"; export const PlanDetailsSection = () => { const { billingAccount, organization } = useContext(OrganizationContext); diff --git a/web/apps/admin/src/pages/organizations/details/side-panel/side-panel.module.css b/web/sdk/admin/views/organizations/details/side-panel/side-panel.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/side-panel/side-panel.module.css rename to web/sdk/admin/views/organizations/details/side-panel/side-panel.module.css diff --git a/web/apps/admin/src/pages/organizations/details/side-panel/tokens-details-section.tsx b/web/sdk/admin/views/organizations/details/side-panel/tokens-details-section.tsx similarity index 93% rename from web/apps/admin/src/pages/organizations/details/side-panel/tokens-details-section.tsx rename to web/sdk/admin/views/organizations/details/side-panel/tokens-details-section.tsx index 63013d6cb8..bf819818b4 100644 --- a/web/apps/admin/src/pages/organizations/details/side-panel/tokens-details-section.tsx +++ b/web/sdk/admin/views/organizations/details/side-panel/tokens-details-section.tsx @@ -1,7 +1,6 @@ import { List, Text, Flex } from "@raystack/apsara"; import styles from "./side-panel.module.css"; -import CoinIcon from "~/assets/icons/coin.svg?react"; -import CoinColoredIcon from "~/assets/icons/coin-colored.svg?react"; +import { MixIcon } from "@radix-ui/react-icons"; import { useContext, useEffect } from "react"; import Skeleton from "react-loading-skeleton"; import { OrganizationContext } from "../contexts/organization-context"; @@ -53,7 +52,7 @@ export const TokensDetailsSection = () => { ) : ( - + {tokenBalance} )} @@ -68,7 +67,7 @@ export const TokensDetailsSection = () => { ) : ( - + {tokensUsed} )} diff --git a/web/apps/admin/src/pages/organizations/details/tokens/columns.tsx b/web/sdk/admin/views/organizations/details/tokens/columns.tsx similarity index 95% rename from web/apps/admin/src/pages/organizations/details/tokens/columns.tsx rename to web/sdk/admin/views/organizations/details/tokens/columns.tsx index 0d3bb882e1..c9f78f3e0b 100644 --- a/web/apps/admin/src/pages/organizations/details/tokens/columns.tsx +++ b/web/sdk/admin/views/organizations/details/tokens/columns.tsx @@ -1,5 +1,5 @@ import styles from "./tokens.module.css"; -import { NULL_DATE } from "~/utils/constants"; +import { NULL_DATE } from "../../../../utils/constants"; import dayjs from "dayjs"; import { Avatar, @@ -15,7 +15,7 @@ import { isNullTimestamp, TimeStamp, timestampToDate, -} from "~/utils/connect-timestamp"; +} from "../../../../utils/connect-timestamp"; export const getColumns = (): DataTableColumnDef< SearchOrganizationTokensResponse_OrganizationToken, diff --git a/web/apps/admin/src/pages/organizations/details/tokens/index.tsx b/web/sdk/admin/views/organizations/details/tokens/index.tsx similarity index 94% rename from web/apps/admin/src/pages/organizations/details/tokens/index.tsx rename to web/sdk/admin/views/organizations/details/tokens/index.tsx index 2bc97423e1..ec58acf768 100644 --- a/web/apps/admin/src/pages/organizations/details/tokens/index.tsx +++ b/web/sdk/admin/views/organizations/details/tokens/index.tsx @@ -5,14 +5,11 @@ import { CoinIcon } from "@raystack/apsara/icons"; import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; import { useContext, useEffect, useMemo, useState } from "react"; import { OrganizationContext } from "../contexts/organization-context"; -import PageTitle from "~/components/page-title"; +import { PageTitle } from "../../../../components/PageTitle"; import { AdminServiceQueries } from "@raystack/proton/frontier"; import { useInfiniteQuery } from "@connectrpc/connect-query"; -import { - transformDataTableQueryToRQLRequest, - getConnectNextPageParam, - DEFAULT_PAGE_SIZE, -} from "@raystack/frontier/admin"; +import { getConnectNextPageParam, DEFAULT_PAGE_SIZE } from "../../../../utils/connect-pagination"; +import { transformDataTableQueryToRQLRequest } from "../../../../utils/transform-query"; import { getColumns } from "./columns"; import { useDebounceValue } from "usehooks-ts"; diff --git a/web/apps/admin/src/pages/organizations/details/tokens/tokens.module.css b/web/sdk/admin/views/organizations/details/tokens/tokens.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/details/tokens/tokens.module.css rename to web/sdk/admin/views/organizations/details/tokens/tokens.module.css diff --git a/web/apps/admin/src/pages/organizations/details/types.ts b/web/sdk/admin/views/organizations/details/types.ts similarity index 100% rename from web/apps/admin/src/pages/organizations/details/types.ts rename to web/sdk/admin/views/organizations/details/types.ts diff --git a/web/apps/admin/src/pages/organizations/list/columns.tsx b/web/sdk/admin/views/organizations/list/columns.tsx similarity index 99% rename from web/apps/admin/src/pages/organizations/list/columns.tsx rename to web/sdk/admin/views/organizations/list/columns.tsx index 40e08dcebb..b3b57abebb 100644 --- a/web/apps/admin/src/pages/organizations/list/columns.tsx +++ b/web/sdk/admin/views/organizations/list/columns.tsx @@ -14,7 +14,7 @@ import { isNullTimestamp, TimeStamp, timestampToDate, -} from "~/utils/connect-timestamp"; +} from "../../../utils/connect-timestamp"; import dayjs from "dayjs"; import styles from "./list.module.css"; diff --git a/web/apps/admin/src/pages/organizations/list/create.tsx b/web/sdk/admin/views/organizations/list/create.tsx similarity index 91% rename from web/apps/admin/src/pages/organizations/list/create.tsx rename to web/sdk/admin/views/organizations/list/create.tsx index d874a0da47..72dae3b50b 100644 --- a/web/apps/admin/src/pages/organizations/list/create.tsx +++ b/web/sdk/admin/views/organizations/list/create.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import styles from "./list.module.css"; import { Button, @@ -12,14 +12,12 @@ import { Label, } from "@raystack/apsara"; import { Cross1Icon } from "@radix-ui/react-icons"; -import { AvatarUpload } from "@raystack/frontier/react"; -import { AppContext } from "~/contexts/App"; +import { AvatarUpload } from "../../../../react/components/avatar-upload"; import { z } from "zod"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation } from "@connectrpc/connect-query"; import { AdminServiceQueries } from "@raystack/proton/frontier"; -import { useNavigate } from "react-router-dom"; const orgCreateSchema = z .object({ @@ -43,25 +41,31 @@ const orgCreateSchema = z type OrgCreateSchema = z.infer; -async function loadCountries() { - const data = await import("~/assets/data/countries.json"); - return data.default; -} - const otherTypePrefix = "Other - "; -export function CreateOrganizationPanel({ onClose }: { onClose: () => void }) { - const { config } = useContext(AppContext); - const [countries, setCountries] = useState([]); - const navigate = useNavigate(); +export type CreateOrganizationPanelProps = { + onClose: () => void; + organizationTypes?: string[]; + appUrl?: string; + countries?: string[]; + onSuccess?: (orgId: string) => void; +}; - const industries = config?.organization_types || []; +export function CreateOrganizationPanel({ + onClose, + organizationTypes = [], + appUrl = "", + countries: countriesProp = [], + onSuccess, +}: CreateOrganizationPanelProps) { + const [countries, setCountries] = useState(countriesProp); + const industries = organizationTypes; useEffect(() => { - loadCountries().then((list) => - setCountries(list.map((country) => country.name)), - ); - }, []); + if (countriesProp.length > 0) { + setCountries(countriesProp); + } + }, [countriesProp]); const { handleSubmit, @@ -109,8 +113,8 @@ export function CreateOrganizationPanel({ onClose }: { onClose: () => void }) { const orgResp = await createOrganization({ body: payload }); const organization = orgResp.organization; - if (organization) { - navigate(`/organizations/${organization.id}`); + if (organization?.id) { + onSuccess?.(organization.id); } } catch (err: unknown) { console.error("Unable to create new org:", err); @@ -173,7 +177,7 @@ export function CreateOrganizationPanel({ onClose }: { onClose: () => void }) { /> { + return ( + } + /> + ); +}; + +const DEFAULT_SORT: DataTableSort = { name: "created_at", order: "desc" }; +const INITIAL_QUERY: DataTableQuery = { + offset: 0, + limit: DEFAULT_PAGE_SIZE, +}; + +export type OrganizationListProps = { + appName?: string; + onNavigateToOrg?: (id: string) => void; + onExportCsv?: () => Promise; + organizationTypes?: string[]; + appUrl?: string; + countries?: string[]; +}; + +export const OrganizationList = ({ + appName, + onNavigateToOrg, + onExportCsv, + organizationTypes = [], + appUrl, + countries = [], +}: OrganizationListProps = {}) => { + const [showCreatePanel, setShowCreatePanel] = useState(false); + + const [tableQuery, setTableQuery] = useDebouncedState( + INITIAL_QUERY, + 200, + ); + + // Transform the DataTableQuery to RQLRequest format + const query = transformDataTableQueryToRQLRequest(tableQuery, { + fieldNameMapping: { + createdBy: "created_by", + planName: "plan_name", + subscriptionCycleEndAt: "subscription_cycle_end_at", + paymentMode: "payment_mode", + subscriptionState: "subscription_state", + createdAt: "created_at", + }, + }); + + const { + data: infiniteData, + isLoading, + isFetchingNextPage, + fetchNextPage, + error, + isError, + } = useInfiniteQuery( + AdminServiceQueries.searchOrganizations, + { query: query }, + { + pageParamKey: "query", + getNextPageParam: lastPage => + getConnectNextPageParam(lastPage, { query: query }, "organizations"), + staleTime: 0, + refetchOnWindowFocus: false, + retry: 1, + retryDelay: 1000, + }, + ); + + const { data: plans = [], isLoading: isPlansLoading, error: plansError } = useQuery( + FrontierServiceQueries.listPlans, + create(ListPlansRequestSchema, {}), + { + select: (data) => data?.plans || [], + }, + ); + + // Log error if it occurs + useEffect(() => { + if (plansError) { + console.error("Failed to fetch plans:", plansError); + } + }, [plansError]); + + const data = + infiniteData?.pages?.flatMap(page => page?.organizations || []) || []; + + const groupCountMap = infiniteData + ? getGroupCountMapFromFirstPage(infiniteData) + : {}; + + const onTableQueryChange = (newQuery: DataTableQuery) => { + setTableQuery({ + ...newQuery, + offset: 0, + limit: newQuery.limit || DEFAULT_PAGE_SIZE, + }); + }; + + const handleLoadMore = async () => { + try { + await fetchNextPage(); + } catch (error) { + console.error("Error loading more organizations:", error); + } + }; + + function closeCreateOrgPanel() { + setShowCreatePanel(false); + } + + function openCreateOrgPanel() { + setShowCreatePanel(true); + } + + const columns = getColumns({ plans, groupCountMap }); + + const loading = isLoading || isPlansLoading || isFetchingNextPage; + + if (isError) { + return ( + <> + + } + heading="Error Loading Organizations" + subHeading={ + error?.message || + "Something went wrong while loading organizations. Please try again." + } + /> + + ); + } + + const tableClassName = + data.length || loading ? styles["table"] : styles["table-empty"]; + + function onRowClick(row: SearchOrganizationsResponse_OrganizationResult) { + if (row.id && onNavigateToOrg) onNavigateToOrg(row.id); + } + return ( + <> + {showCreatePanel ? ( + { + closeCreateOrgPanel(); + onNavigateToOrg?.(id); + }} + /> + ) : null} + + + + + + } + rowHeight={48} + groupHeaderHeight={48} + /> + + + + ); +}; diff --git a/web/apps/admin/src/pages/organizations/list/list.module.css b/web/sdk/admin/views/organizations/list/list.module.css similarity index 100% rename from web/apps/admin/src/pages/organizations/list/list.module.css rename to web/sdk/admin/views/organizations/list/list.module.css diff --git a/web/apps/admin/src/pages/organizations/list/navbar.tsx b/web/sdk/admin/views/organizations/list/navbar.tsx similarity index 78% rename from web/apps/admin/src/pages/organizations/list/navbar.tsx rename to web/sdk/admin/views/organizations/list/navbar.tsx index 8139226bf3..1535329423 100644 --- a/web/apps/admin/src/pages/organizations/list/navbar.tsx +++ b/web/sdk/admin/views/organizations/list/navbar.tsx @@ -15,19 +15,17 @@ import { PlusIcon, } from "@radix-ui/react-icons"; import React, { useState } from "react"; -import { exportCsvFromStream } from "~/utils/helper"; -import { clients } from "~/connect/clients"; - -const adminClient = clients.admin({ useBinary: true }); interface OrganizationsNavabarProps { searchQuery?: string; openCreatePanel: () => void; + onExportCsv?: () => Promise; } export const OrganizationsNavabar = ({ searchQuery, openCreatePanel, + onExportCsv, }: OrganizationsNavabarProps) => { const [showSearch, setShowSearch] = useState(searchQuery ? true : false); const [isDownloading, setIsDownloading] = useState(false); @@ -44,13 +42,10 @@ export const OrganizationsNavabar = ({ } async function onDownloadClick() { + if (!onExportCsv) return; try { setIsDownloading(true); - await exportCsvFromStream( - adminClient.exportOrganizations, - {}, - "organizations.csv" - ); + await onExportCsv(); } catch (error) { console.error(error); } finally { @@ -93,15 +88,17 @@ export const OrganizationsNavabar = ({ )} - - {isDownloading ? : } - + {onExportCsv ? ( + + {isDownloading ? : } + + ) : null} ); diff --git a/web/lib/admin/views/plans/columns.tsx b/web/sdk/admin/views/plans/columns.tsx similarity index 100% rename from web/lib/admin/views/plans/columns.tsx rename to web/sdk/admin/views/plans/columns.tsx diff --git a/web/lib/admin/views/plans/details.tsx b/web/sdk/admin/views/plans/details.tsx similarity index 100% rename from web/lib/admin/views/plans/details.tsx rename to web/sdk/admin/views/plans/details.tsx diff --git a/web/lib/admin/views/plans/header.tsx b/web/sdk/admin/views/plans/header.tsx similarity index 100% rename from web/lib/admin/views/plans/header.tsx rename to web/sdk/admin/views/plans/header.tsx diff --git a/web/lib/admin/views/plans/index.tsx b/web/sdk/admin/views/plans/index.tsx similarity index 100% rename from web/lib/admin/views/plans/index.tsx rename to web/sdk/admin/views/plans/index.tsx diff --git a/web/lib/admin/views/plans/plans.module.css b/web/sdk/admin/views/plans/plans.module.css similarity index 100% rename from web/lib/admin/views/plans/plans.module.css rename to web/sdk/admin/views/plans/plans.module.css diff --git a/web/sdk/admin/views/preferences/PreferencesView.tsx b/web/sdk/admin/views/preferences/PreferencesView.tsx new file mode 100644 index 0000000000..eb12c27576 --- /dev/null +++ b/web/sdk/admin/views/preferences/PreferencesView.tsx @@ -0,0 +1,86 @@ +import { Flex, EmptyState } from "@raystack/apsara"; +import { createQueryOptions, useTransport } from "@connectrpc/connect-query"; +import { + AdminServiceQueries, + FrontierServiceQueries, + Preference, + PreferenceTrait, + ListPreferencesResponse, + DescribePreferencesResponse, +} from "@raystack/proton/frontier"; +import { ExclamationTriangleIcon } from "@radix-ui/react-icons"; +import { useQueries } from "@tanstack/react-query"; +import type { ConnectError } from "@connectrpc/connect"; +import PreferencesList from "./index"; +import PreferenceDetails from "./details"; + +export type PreferencesViewProps = { + selectedPreferenceName?: string; + onCloseDetail?: () => void; +}; + +export default function PreferencesView({ + selectedPreferenceName, + onCloseDetail, +}: PreferencesViewProps = {}) { + const transport = useTransport(); + + const [preferencesQuery, traitsQuery] = useQueries({ + queries: [ + { + ...createQueryOptions(AdminServiceQueries.listPreferences, {}, { transport }), + staleTime: Infinity, + }, + { + ...createQueryOptions(FrontierServiceQueries.describePreferences, {}, { transport }), + staleTime: Infinity, + }, + ], + }); + + const preferences = ((preferencesQuery.data as ListPreferencesResponse)?.preferences || []) as Preference[]; + const traits = ((traitsQuery.data as DescribePreferencesResponse)?.traits || []) as PreferenceTrait[]; + const isLoading = preferencesQuery.isLoading || traitsQuery.isLoading; + const isError = preferencesQuery.isError || traitsQuery.isError; + const error = (preferencesQuery.error || traitsQuery.error) as ConnectError | null; + + if (isError) { + console.error("ConnectRPC Error:", error); + return ( + + } + heading="Error Loading Preferences" + subHeading={ + error?.message || + "Something went wrong while loading preferences. Please try again." + } + /> + + ); + } + + if (selectedPreferenceName) { + return ( + + + + ); + } + + return ( + + + + ); +} diff --git a/web/apps/admin/src/containers/preferences.list/columns.tsx b/web/sdk/admin/views/preferences/columns.tsx similarity index 99% rename from web/apps/admin/src/containers/preferences.list/columns.tsx rename to web/sdk/admin/views/preferences/columns.tsx index f04d3ec235..d3c17a9929 100644 --- a/web/apps/admin/src/containers/preferences.list/columns.tsx +++ b/web/sdk/admin/views/preferences/columns.tsx @@ -1,6 +1,5 @@ import type { DataTableColumnDef } from "@raystack/apsara"; import { Preference, PreferenceTrait } from "@raystack/proton/frontier"; - import { Link } from "react-router-dom"; import styles from "./preferences.module.css"; diff --git a/web/apps/admin/src/containers/preferences.list/details.tsx b/web/sdk/admin/views/preferences/details.tsx similarity index 91% rename from web/apps/admin/src/containers/preferences.list/details.tsx rename to web/sdk/admin/views/preferences/details.tsx index 0b000e9f0d..09b1a7d5d4 100644 --- a/web/apps/admin/src/containers/preferences.list/details.tsx +++ b/web/sdk/admin/views/preferences/details.tsx @@ -1,4 +1,4 @@ -import PageHeader from "~/components/page-header"; +import { PageHeader } from "../../components/PageHeader"; import { Grid, Button, @@ -9,7 +9,6 @@ import { InputField, } from "@raystack/apsara"; import { useCallback, useEffect, useState } from "react"; -import { useOutletContext, useParams } from "react-router-dom"; import Skeleton from "react-loading-skeleton"; import dayjs from "dayjs"; import { toast } from "sonner"; @@ -20,16 +19,6 @@ import { timestampDate } from "@bufbuild/protobuf/wkt"; import { useQueryClient } from "@tanstack/react-query"; import { create } from "@bufbuild/protobuf"; -interface ContextType { - preferences: Preference[]; - traits: PreferenceTrait[]; - isLoading: boolean; -} - -export function usePreferences() { - return useOutletContext(); -} - interface PreferenceValueProps { trait: PreferenceTrait; value: string; @@ -62,11 +51,23 @@ function PreferenceValue({ value, trait, onChange }: PreferenceValueProps) { } } -export default function PreferenceDetails() { - const { name } = useParams(); +export type PreferenceDetailsProps = { + selectedName: string; + preferences: Preference[]; + traits: PreferenceTrait[]; + isLoading: boolean; + onCloseDetail?: () => void; +}; + +export default function PreferenceDetails({ + selectedName: name, + preferences, + traits, + isLoading, + onCloseDetail, +}: PreferenceDetailsProps) { const [value, setValue] = useState(""); const [originalValue, setOriginalValue] = useState(""); - const { preferences, traits, isLoading } = usePreferences(); const preference = preferences?.find((p) => p.name === name); const trait = traits?.find((t) => t.name === name); @@ -118,14 +119,8 @@ export default function PreferenceDetails() { const pageHeader = { title: "Preference", breadcrumb: [ - { - href: `/preferences`, - name: `Preferences`, - }, - { - href: `/preferences/${trait?.name}`, - name: `${trait?.title}`, - }, + { name: "Preferences", href: "#" }, + { name: trait?.title ?? name }, ], }; @@ -200,6 +195,7 @@ export default function PreferenceDetails() { { if (item.name === "Preferences") onCloseDetail(); } : undefined} style={{ borderBottom: "1px solid var(--rs-color-border-base-primary)", gap: "16px", @@ -230,12 +226,11 @@ export default function PreferenceDetails() { )} {trait ? ( - +