Activities of "maliming"

Hi,

Good questions, let me take them in order.

On the proxy and app-service ownership: the backend doesn't change between Angular and React, and you don't need to move your module's app services into the main application module. Your module keeps its own .Application.Contracts, .Application and .HttpApi layers exactly as before. When the host references your module's packages and depends on its module class, ABP brings the module's services and HTTP APIs into the host at runtime — that's the normal module dependency mechanism, and it's identical whether the host UI is Angular or React. So /api/abp/api-definition still exposes your module's endpoints the same way.

Where it's not symmetric is the frontend packaging. In the Angular world your module ships an npm package that includes the generated proxies and a provideXxx() provider, so the consumer gets the client and the routes from that package. React doesn't have that built-in module pipeline yet. On the React side the API layer is source-owned under src/lib/api on a shared Axios instance, and you regenerate the typed client from the running host (the template's generate-proxy script) rather than from a per-module package. You can still publish a React npm package that ships its own API client, but that's a custom convention you own, not an ABP-provided module integration contract. So: backend symmetry yes, frontend packaging symmetry no, and nothing needs to be merged into the main app module.

On the naming and direction: "classic" and "modern" refer to the solution template system, not an Angular-to-React migration. The classic family is the Single-Layer / Layered templates; the modern family is the ABP Studio wizard architectures (Simple Monolith, Layered Monolith, Modular Monolith, Microservice). React is available only in the modern template system, which is where the "Angular = classic, React = modern" impression comes from, but it isn't a signal that Angular is being retired. Angular is actively maintained — the roadmap includes Angular UI fixes and proxy generation improvements, and Angular SSR is already documented.

React was added as an additional first-class UI option, not a replacement. The longer-term "framework-neutral module UI" idea you're describing matches the public Hybrid UI System backlog item: re-using a module's UI across technologies so a module doesn't have to re-implement a native UI for each stack. I'd read that as the direction being explored rather than a committed plan, and it's the right place to weigh in:

  • https://github.com/abpframework/abp/issues/23102

On what to build today: if your priority is shipping a reusable module with an officially-supported frontend integration right now, Angular is the safer choice — it has the mature module-UI packaging story (module project, dev-app, npm package, provideXxx() provider, generated proxies). React is a great choice for application-owned UI, and you can make a module's React UI work with the custom npm convention from my previous reply, but ABP doesn't yet provide the same module frontend packaging/injection for React.

I wouldn't block your backend/module design on the Hybrid UI work, since it's still a backlog item with no committed version. The backend module is the same either way, so you can build it cleanly now and pick the UI packaging based on what you need to deliver — Angular if you need a distributable module frontend today, React (with the custom convention) if the UI is mainly for your own app.

Thanks

👍

Great 👍

Great 👍

Hi,

You're reading it right. In the modern (React) stack there's no "a module ships its React UI and it plugs into the host app" mechanism today, the way the classic Angular template does it. That's not something wrong with your setup, it's where the modern React UI currently stands, and it's a gap the team has on the roadmap. Let me answer each question with the current state, show the pattern that works today, and then point you at the direction.

1 and 3) There's no official packaging or forRoot()/forChild()-style mechanism for React. Routing in the template is hand-written with TanStack Router in src/routes/router.tsx, and the menu is a static routeConfig array in src/lib/routing/route-config.ts that the sidebar reads. There's no provider/registry layer a module can register into, so the host adds routes and menu items explicitly. For contrast: the module template does generate UI for MVC, Blazor and Angular, and Angular even ships the provideXxx() route-provider plus npm packages you're thinking of. React just isn't one of the module-template UI options yet, which is exactly why abp new-module produces no React frontend.

  1. Shipping a React npm package alongside your NuGet packages is the practical approach today, but it's your own convention rather than a standardized ABP contract. Keep the package "dumb": export the page components plus a small menu-metadata array, and let the host do the route registration (the route tree, rootRoute, and the permission guards are host-local, so the package shouldn't import host internals). This is the same "add a page" / "add a sidebar item" flow the customization docs describe:
// In your module's npm package, e.g. @acme/product-management-react
export { ProductsPage } from './pages/ProductsPage'

export const productMenuItems = [
  {
    path: '/products',
    nameKey: 'ProductManagement::Products',
    requiredPolicy: 'ProductManagement.Products',
  },
]

The host wires the route in src/routes/router.tsx:

import { ProductsPage } from '@acme/product-management-react'

const productsRoute = createRoute({
  getParentRoute: () => rootRoute,
  path: '/products',
  component: ProductsPage,
  beforeLoad: createPermissionGuard('ProductManagement.Products'),
})

const routeTree = rootRoute.addChildren([
  indexRoute,
  forbiddenRoute,
  accountRoute,
  identityRoute,
  productsRoute, // your module's route
])

and the menu item in src/lib/routing/route-config.ts:

import { productMenuItems } from '@acme/product-management-react'

export const routeConfig: RouteConfigItem[] = [
  // ...existing items
  ...productMenuItems,
]

A few things to keep the package portable:

  • Set react, react-dom, @tanstack/react-router, @tanstack/react-query, react-i18next (and lucide-react if you use icons) as peerDependencies so you don't pull a second copy into the host.
  • Your localization keys (nameKey) need to reach the host's i18n. Ship them with the package and have the host merge them into its resources.
  • For API calls, reuse the host's configured Axios/api layer rather than a standalone client in your package. The template's Axios already handles auth tokens, the current tenant, language headers and 401/403 behavior, and a separate client would miss all of that. After your module's HTTP API is added, the host runs npm run generate-proxy (abp generate-proxy -t js) to get the typed client. The proxy isn't generated from the NuGet package automatically.
  1. The Admin Console is a separate, pre-built React app delivered as the Volo.Abp.AdminConsole NuGet package and served at /admin-console. It decides which module pages to show by checking the backend services present in the host, but the page set is built-in and there's no supported way for a third-party module to inject its own pages into it. So your module's management pages go into your own React app instead. You can put them behind an auth/permission layout (like the generated IdentityLayout) to make them admin-style.

  2. We don't currently publish per-module React UI packages/source the way the classic Angular template did. In the modern stack the commercial modules' admin UI is bundled inside the pre-built Admin Console, not distributed as referenceable React source. The closest reference you have is the Identity management UI the template generates into your own app under src/pages/identity and src/components/identity — that's the same pattern to follow for your module's pages.

On the direction, which is the part you actually care about: the source-owned model is deliberate for your app's own pages, but modules contributing UI into the host for React is a recognized gap, and it's tracked on the public roadmap. The way ABP thinks about a UI-framework integration is in three parts: framework-level integration (auth, permissions, localization, settings), the theme/component stack, and reusable module UI. For React the first two are in place as of 10.4; the third — module UI that a module can contribute into another app — is the missing piece. The plan to address it is the Hybrid UI System: reusing a module's UI across technologies so it doesn't have to be re-implemented natively for every stack. The public roadmap lists foundational "Hybrid UI / page embedding infrastructure" under v10.6, and the broader direction is tracked in the issue below. It's still a backlog item, so please take it as the direction being worked toward, not a committed React module-UI package or a specific version:

  • https://abp.io/docs/latest/release-info/road-map
  • https://github.com/abpframework/abp/issues/23102

That issue is worth following and commenting on — your exact scenario (a distributable module shipping both backend and frontend, targetable to the app or an admin surface) is the use case it's meant to solve, and feedback there directly shapes the design.

Thanks

Hi,

You're right, this is a casing bug on our side. The module registry that abp add-module reads reports the PayPal Domain.Shared module with the wrong namespace casing — Volo.Payment.Paypal instead of Volo.Payment.PayPal. The CLI writes that value as-is, so it adds using Volo.Payment.Paypal; to your *.Domain.Shared project, which doesn't match the real namespace and the build fails with CS0234/CS0246. That same casing inconsistency is what you're seeing as the two namespace groups in the API reference.

Your manual fix is exactly the right one — keep using:

using Volo.Payment.PayPal;

We'll correct the casing in the module's package metadata so it's consistent and add-module no longer needs the manual edit. Thanks for digging into it and sharing the screenshot.

Thanks

Hi,

abp.passkey.serializeCredential isn't defined inline or by a bundle contributor — it lives in a separate embedded file, /Pages/Account/passkey-helpers.js, inside Volo.Abp.Account.Pro.Public.Web. The stock Login.cshtml adds it to the login script bundle right before Login.js:

<abp-script-bundle name="@typeof(Volo.Abp.Account.Public.Web.Pages.Account.LoginModel).FullName">
    <abp-script src="/Pages/Account/passkey-helpers.js" />
    <abp-script src="/Pages/Account/Login.js" />
</abp-script-bundle>

When you override Login.cshtml, your <abp-script-bundle> replaces the whole bundle declaration for that page, so dropping the first line is what leaves abp.passkey undefined — there's no separate contributor adding it for the login page, so nothing puts it back. Add the line back before Login.js in your custom cshtml and the error goes away; you can then drop your abp.passkey shim.

For your three asks:

  1. It's /Pages/Account/passkey-helpers.js, pulled into the login bundle through that <abp-script> line in the stock page — not a contributor, not an inline script.
  2. Fair point — we'll add a note to the login/passkey customization docs that this line has to be preserved when you replace the page.
  3. Nothing is failing to fire; the helper only reaches the login bundle via the cshtml, so overriding the view without that line simply removes it.

One note on your shim: serializeCredential in passkey-helpers.js builds the JSON by hand on purpose instead of calling PublicKeyCredential.toJSON(), because some password managers (1Password being one) don't implement toJSON() correctly. Since 1Password is in your test set, the stock helper is safer than a toJSON() shim. It also already serializes clientExtensionResults, so once the helper is back in the bundle you can check whether your separate PasskeyCompatibility.js patch is still doing anything.

Thanks

Hi,

You're right, this is a provider-order bug on our side. When you pass withThemeLeptonXOptions(...), that feature internally calls provideLpx(...) again, which re-registers the default (English) translate service after the ABP one. Angular DI keeps the last provider for a given token, so the default service wins and the settings menu ("General Settings", "Language Options", ...) falls back to its hardcoded English labels.

Your workaround is the right approach. Just import LPX_TRANSLATE_PROVIDER from @volosoft/abp.ng.theme.lepton-x and add it after provideThemeLeptonX(...) so the ABP translate service stays the active one:

import {
  provideThemeLeptonX,
  withThemeLeptonXOptions,
  LPX_TRANSLATE_PROVIDER,
} from '@volosoft/abp.ng.theme.lepton-x';

providers: [
  provideThemeLeptonX(
    withThemeLeptonXOptions({
      defaultTheme: 'dim',
    }),
  ),
  LPX_TRANSLATE_PROVIDER,
]

We'll fix this inside provideThemeLeptonX in the next LeptonX release so the extra provider won't be needed.

Thanks

ok

Hi,

You're right — provideCmsKitAdminConfig() and provideCmsKitProAdminConfig() both drag the heavy admin entry into the initial bundle, exactly as you described. The chain runs through setting-tab.provider.ts, which top-level imports CmsSettingsComponent from @abp/ng.cms-kit/admin / @volo/abp.ng.cms-kit-pro/admin. Those entries export * from ./components, and a few of those components (page-form, blog-post-form, global-resources, and Pro's faq-question-modal) statically import ToastuiEditorComponent and CodeMirrorEditorComponent. So registering the menu eagerly is enough to anchor the editor + grids in the initial graph, even when the actual /cms routes are lazy-loaded.

We'll track this and move the menu/route registration out of the heavy entry in a future release — the model @volo/abp.ng.identity/config already uses, where IdentitySettingsComponent lives inside the config entry itself.

In the meantime, drop this file into your app and call these in place of provideCmsKitAdminConfig() / provideCmsKitProAdminConfig():

import { eLayoutType, RoutesService } from '@abp/ng.core';
import {
  EnvironmentProviders,
  inject,
  makeEnvironmentProviders,
  provideAppInitializer,
} from '@angular/core';

// Enum values inlined from @abp/ng.cms-kit/admin/config and
// @volo/abp.ng.cms-kit-pro/admin/config. Importing any symbol from those
// entries pulls setting-tab.provider.ts in, which top-level imports
// CmsSettingsComponent from the heavy admin entry — that's the import that
// drags the editor + grids into the initial bundle.
const CmsKitRouteNames = {
  Cms:             'CmsKit::Menu:CMS',
  BlogPosts:       'CmsKit::BlogPosts',
  Blogs:           'CmsKit::Blogs',
  Comments:        'CmsKit::CmsKit.Comments',
  GlobalResources: 'CmsKit::GlobalResources',
  Menus:           'CmsKit::Menus',
  Pages:           'CmsKit::Pages',
  Tags:            'CmsKit::CmsKit.Tags',
} as const;

const CmsKitPolicyNames = {
  BlogPosts:       'CmsKit.BlogPosts',
  Blogs:           'CmsKit.Blogs',
  Comments:        'CmsKit.Comments',
  GlobalResources: 'CmsKit.GlobalResources',
  Menus:           'CmsKit.Menus',
  Pages:           'CmsKit.Pages',
  Tags:            'CmsKit.Tags',
} as const;

const CmsKitProRouteNames = {
  Cms:           'CmsKit::Menu:CMS',
  Newsletters:   'CmsKit::NewsletterUsers',
  Polls:         'CmsKit::Polls',
  Faqs:          'CmsKit::Faqs',
  PageFeedbacks: 'CmsKit::PageFeedbacks',
  UrlShorting:   'CmsKit::UrlForwarding',
} as const;

const CmsKitProPolicyNames = {
  Cms:           'CmsKit.Newsletter || CmsKit.Poll || CmsKit.Faq || CmsKit.PageFeedback || CmsKit.UrlShorting',
  Newsletters:   'CmsKit.Newsletter',
  Polls:         'CmsKit.Poll',
  Faqs:          'CmsKit.Faq',
  PageFeedbacks: 'CmsKit.PageFeedback',
  UrlShorting:   'CmsKit.UrlShorting',
} as const;

export function provideCmsKitAdminRoutesLight(): EnvironmentProviders {
  return makeEnvironmentProviders([
    provideAppInitializer(() => {
      const routes = inject(RoutesService);
      routes.add([
        { path: '/cms/blog-posts',       name: CmsKitRouteNames.BlogPosts,       parentName: CmsKitRouteNames.Cms, order: 1,  layout: eLayoutType.application, iconClass: 'fa fa-file-alt', requiredPolicy: CmsKitPolicyNames.BlogPosts },
        { path: '/cms/blogs',            name: CmsKitRouteNames.Blogs,           parentName: CmsKitRouteNames.Cms, order: 2,  layout: eLayoutType.application, iconClass: 'fa fa-blog',     requiredPolicy: CmsKitPolicyNames.Blogs },
        { path: '/cms/comments',         name: CmsKitRouteNames.Comments,        parentName: CmsKitRouteNames.Cms, order: 3,  layout: eLayoutType.application, iconClass: 'fa fa-comments', requiredPolicy: CmsKitPolicyNames.Comments },
        { path: '/cms/global-resources', name: CmsKitRouteNames.GlobalResources, parentName: CmsKitRouteNames.Cms, order: 5,  layout: eLayoutType.application, iconClass: 'fa fa-globe',    requiredPolicy: CmsKitPolicyNames.GlobalResources },
        { path: '/cms/menus',            name: CmsKitRouteNames.Menus,           parentName: CmsKitRouteNames.Cms, order: 6,  layout: eLayoutType.application, iconClass: 'fa fa-bars',     requiredPolicy: CmsKitPolicyNames.Menus },
        { path: '/cms/pages',            name: CmsKitRouteNames.Pages,           parentName: CmsKitRouteNames.Cms, order: 9,  layout: eLayoutType.application, iconClass: 'fa fa-file',     requiredPolicy: CmsKitPolicyNames.Pages },
        { path: '/cms/tags',             name: CmsKitRouteNames.Tags,            parentName: CmsKitRouteNames.Cms, order: 11, layout: eLayoutType.application, iconClass: 'fa fa-tags',     requiredPolicy: CmsKitPolicyNames.Tags },
      ]);
    }),
  ]);
}

export function provideCmsKitProAdminRoutesLight(): EnvironmentProviders {
  return makeEnvironmentProviders([
    provideAppInitializer(() => {
      const routes = inject(RoutesService);
      routes.add([
        { path: '/cms',                name: CmsKitProRouteNames.Cms,                                                       order: 100, layout: eLayoutType.application, iconClass: 'fa fa-newspaper',     requiredPolicy: CmsKitProPolicyNames.Cms },
        { path: '/cms/faqs',           name: CmsKitProRouteNames.Faqs,           parentName: CmsKitProRouteNames.Cms, order: 4,  layout: eLayoutType.application, iconClass: 'fa fa-question-circle', requiredPolicy: CmsKitProPolicyNames.Faqs },
        { path: '/cms/newsletters',    name: CmsKitProRouteNames.Newsletters,    parentName: CmsKitProRouteNames.Cms, order: 7,  layout: eLayoutType.application, iconClass: 'fa fa-newspaper',     requiredPolicy: CmsKitProPolicyNames.Newsletters },
        { path: '/cms/page-feedbacks', name: CmsKitProRouteNames.PageFeedbacks,  parentName: CmsKitProRouteNames.Cms, order: 8,  layout: eLayoutType.application, iconClass: 'fa fa-comment',       requiredPolicy: CmsKitProPolicyNames.PageFeedbacks },
        { path: '/cms/polls',          name: CmsKitProRouteNames.Polls,          parentName: CmsKitProRouteNames.Cms, order: 10, layout: eLayoutType.application, iconClass: 'fa fa-poll',          requiredPolicy: CmsKitProPolicyNames.Polls },
        { path: '/cms/url-shorting',   name: CmsKitProRouteNames.UrlShorting,    parentName: CmsKitProRouteNames.Cms, order: 12, layout: eLayoutType.application, iconClass: 'fa fa-forward',       requiredPolicy: CmsKitProPolicyNames.UrlShorting },
      ]);
    }),
  ]);
}

A few things to keep in mind when wiring this up:

  • Do not import anything from @abp/ng.cms-kit/admin/config or @volo/abp.ng.cms-kit-pro/admin/config in this file (or anywhere else loaded eagerly). The moment you import any symbol from those entries — even the enum — the whole fesm comes in and brings CmsSettingsComponent (and the heavy admin entry) with it.
  • Only the Pro provider above registers the /cms parent menu node (the OSS provider only registers child items hanging off parentName: 'CmsKit::Menu:CMS'). If you ever stop calling provideCmsKitProAdminRoutesLight(), add an equivalent top-level entry yourself, otherwise the OSS children won't have a parent to attach to. Both Cms names resolve to the same key, so the two route sets render as a single "CMS" group when both are registered.
  • You lose the "Settings → CMS Kit" tab in both modules. The OSS tab renders the Require comment approval toggle; the Pro tab renders Contact settings and Page Feedback settings. The easiest replacement is to expose the switches you actually use from your own settings page, or call the underlying APIs (CommentAdminService.updateSettings from @abp/ng.cms-kit/proxy, ContactSettingService.update and PageFeedbackAdminService.updateSettings from @volo/abp.ng.cms-kit-pro/proxy) directly.
  • Keep your existing loadChildren for @abp/ng.cms-kit/admin / @volo/abp.ng.cms-kit-pro/admin — those still serve the actual pages lazily.

In a stock 10.4.1 + Angular 21.2 + @angular/build project this brings the production initial bundle from 3.13 MB / 738 kB transfer down to 1.84 MB / 424 kB transfer (−1.29 MB / −42%), with the 940 kB toastui chunk moving from initial to lazy.

Thanks

Showing 331 to 340 of 12922 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.