Hi,
The AI Management installer doesn't include the Angular installation info, so ABP Studio skips the Angular part. We'll fix the Angular installation in an upcoming version. For now, you can set it up manually in your Angular app.
If you haven't already, install the package from your angular folder:
yarn add @volo/abp.ng.ai-management@10.7.0
If it's missing, add the config provider to the existing providers array in app.config.ts:
import { provideAIManagementConfig } from '@volo/abp.ng.ai-management/config';
export const appConfig: ApplicationConfig = {
providers: [
// ...
provideAIManagementConfig(),
],
};
If it's missing, add this route to the routes array in app.routes.ts:
{
path: 'ai-management',
loadChildren: () => import('@volo/abp.ng.ai-management').then(c => c.createRoutes()),
},
About the missing using Volo.AIManagement;: ABP Studio checks the module file as plain text before adding a using. If the file has a commented-out using Volo.AIManagement; line, it skips the using but still adds the DependsOn, and the build fails. We'll fix this as well. Adding the using manually, as you did, is the right fix for now.
If your Application module didn't contain that text before the install, could you share the file from before the install (from your git history) and your ABP Studio version? You can send them to liming.ma@volosoft.com if you prefer.
Thanks
Hi,
The fix is in https://github.com/abpframework/abp/pull/26349. It will ship in the next 10.7.x patch release. I don't have an exact date yet, and I'll update this thread when it's out.
After you upgrade:
WaitForTokensOAuthService, checkAccessTokenOrReload, SignInAgainErrorHandler and the waiting code in the storage class). If you still want the refresh token in memory, keep only the short RefreshTokenInMemoryStorageService from https://abp.io/support/questions/10937.provideAbpThemeShared(
withHttpErrorConfig({
signInWithoutToken: true,
}),
),
You mentioned you can't apply the workaround. What's blocking it? If it's something we can change, I'll try to make it simpler until the release.
Thanks
Hi,
Which ABP version is your solution on? The steps below are for upgrading a 10.6.x solution to 10.7.0. If you're on an older version, let me know first.
abp update --version 10.7.0
abp suite update --version 10.7.0
abp update doesn't update the other packages your projects reference directly. Align them with ABP 10.7.0, otherwise you can get restore or startup errors. In a Blazor Server solution created with 10.6, these are:
Microsoft.AspNetCore.*, Microsoft.EntityFrameworkCore.* and Microsoft.Extensions.* packages: 10.0.9 to 10.0.11Blazorise.Bootstrap5 and Blazorise.Icons.FontAwesome: 2.2.1 to 2.3.0The full list is here: https://abp.io/docs/10.7/package-version-changes
Then check the migration guide: https://abp.io/docs/10.7/release-info/migration-guides/abp-10-7. If app.UseAntiforgery(); comes before app.UseAuthorization(); in your Blazor module, move it after.
Build the solution, then start Suite again and regenerate UserMaintenance. Suite reads its templates from the built solution.
Customizable code enabled when you regenerate. Suite keeps your code in the extended files and the custom code blocks of the pages, and your texts in the localization files. Changes made directly in other generated code are overwritten, so check the diff after regenerating.After you regenerate UserMaintenance:
UserMaintenances.Extended.razor.cs. The generated page has supervisorCollection now, so keeping it causes a duplicate definition error.EfCoreUserMaintenanceRepository.cs has using AbpSolution1.using Microsoft.EntityFrameworkCore; and a separate EntityFrameworkCore; line at the top, replace those two lines with:using Microsoft.EntityFrameworkCore;
using AbpSolution1.EntityFrameworkCore;
Then build the solution again and check the page.
The broken usings come from Suite 10.7.0, and we've already fixed it in the 10.7 branch. If you'd upgrade only for this issue, you can also stay on your current version and keep the workaround.
Thanks
Hi,
The breadcrumb path is built only from the folders already loaded in the left tree. When you open a folder from the list before its parent is expanded in the tree, the path can't be built and it falls back to the root.
For now, you can use this patch:
// src/app/file-management.patch.ts
import { Injectable } from '@angular/core';
import type * as FileManagement from '@volo/abp.ng.file-management';
import { DirectoryDescriptorService } from '@volo/abp.ng.file-management/proxy';
import { tap } from 'rxjs';
type Folder = { id?: string; name?: string };
let listedFolderId: string | null = null;
@Injectable()
export class TrackedDirectoryDescriptorService extends DirectoryDescriptorService {
constructor() {
super();
const getContent = this.getContent;
this.getContent = (input, config) =>
getContent(input, config).pipe(tap(() => (listedFolderId = input.id || null)));
}
}
export function patchFileManagementNavigator({ NavigatorService, ROOT_ID }: typeof FileManagement) {
(NavigatorService.prototype as any).updateCurrentFolderPath = function (folder: Folder) {
let path: Folder[] = this.directory.findFullPathOf(folder);
if (folder?.id && path[path.length - 1]?.id !== folder.id) {
const folders: Folder[] = this.navigatedFolders;
const index = folders.findIndex(f => f.id === folder.id);
const parentIndex = folders.findIndex(f => (f.id === ROOT_ID ? null : f.id) === listedFolderId);
if (index > -1) {
path = folders.slice(0, index + 1);
} else if (parentIndex > -1) {
path = [...folders.slice(0, parentIndex + 1), { id: folder.id, name: folder.name }];
} else {
// The list still shows a folder we navigated away from, ignore the click.
path = folders;
}
}
this.next(path);
};
}
Register the service in app.config.ts:
import { DirectoryDescriptorService } from '@volo/abp.ng.file-management/proxy';
import { TrackedDirectoryDescriptorService } from './file-management.patch';
providers: [
{ provide: DirectoryDescriptorService, useClass: TrackedDirectoryDescriptorService },
// ...
],
And patch the navigator when the File Management route is loaded:
// app.routes.ts
import { patchFileManagementNavigator } from './file-management.patch';
{
path: 'file-management',
loadChildren: () =>
import('@volo/abp.ng.file-management').then(c => {
patchFileManagementNavigator(c);
return c.createRoutes();
}),
},
The breadcrumb stays complete and Go up folder goes to the parent, with the tree still collapsed.
This only fixes the navigation, so keep your filter change for now. We'll fix both issues, and you can remove these patches once you upgrade to a version that includes the fix.
Thanks
Hi,
The chat controller in the AI Management client package (ChatCompletionsController) ends up in your app proxies. The invalid ActionResult and ChatRole types come from it.
You don't need to generate proxies for AI Management. The @volo/abp.ng.ai-management package already ships them (@volo/abp.ng.ai-management/proxy).
For now, you can hide this controller from the API definition. The endpoint keeps working, the AI Management Angular UI calls it by URL. It's only hidden from Swagger and proxy generation.
Add this class to your HttpApi.Host project:
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Volo.AIManagement.Client.Controllers;
namespace YourCompany.YourProject;
public class HideChatCompletionsControllerConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
if (controller.ControllerType != typeof(ChatCompletionsController))
{
return;
}
controller.ApiExplorer.IsVisible = false;
foreach (var action in controller.Actions)
{
action.ApiExplorer.IsVisible = false;
}
}
}
Then register it in ConfigureServices of your HttpApi.Host module. The module file needs using Microsoft.AspNetCore.Mvc; and using Microsoft.Extensions.DependencyInjection;:
Configure<MvcOptions>(options =>
{
options.Conventions.Add(new HideChatCompletionsControllerConvention());
});
After that:
src/app/proxy folder in your Angular app. Its generate-proxy.json remembers that you generated ai-management, so it gets generated again on every run.abp generate-proxy -t ng again, without --module.We'll fix the controller in the next release. Your ticket has been refunded.
Thanks
Hi,
Angular CDK 22 opens MatDialog in the browser's top layer by default (it uses the native popover API). Elements outside the top layer always stay behind it, so abp-modal can't get above the dialog with z-index alone, even with 10001.
You can turn this off in your root providers:
import { OVERLAY_DEFAULT_CONFIG } from '@angular/cdk/overlay';
export const appConfig: ApplicationConfig = {
providers: [
// ...your existing providers
{ provide: OVERLAY_DEFAULT_CONFIG, useValue: { usePopover: false } },
],
};
This puts all CDK overlays back to normal z-index stacking.
After that, your windowClass / backdropClass workaround works. You don't need ::ng-deep, just put the CSS in the global styles.scss. Use a more specific selector than the rules in dark.css. Otherwise the result depends on which file loads last.
<abp-modal
[(visible)]="openViewDocument"
[options]="{ windowClass: 'above-cdk-overlay', backdropClass: 'above-cdk-overlay-backdrop' }"
>
...
</abp-modal>
body ngb-modal-backdrop.modal-backdrop.above-cdk-overlay-backdrop {
z-index: 10001 !important;
}
body ngb-modal-window.modal.above-cdk-overlay {
z-index: 10002 !important;
}
If the delete confirmation comes from ABP's ConfirmationService, it isn't an ng-bootstrap modal, so these classes don't apply to it. Add this to the global styles as well:
body .confirmation {
z-index: 10003 !important;
}
The [options] input is typed as NgbModalOptions and is passed to NgbModal.open() as is, so container and the other ng-bootstrap options work too: https://abp.io/docs/latest/framework/ui/angular/modal#options
About abpBody: abp-modal already used the signal contentChild('abpBody') in 8.3.0, the same query as in 10.6. Your original markup with two <ng-template *ngIf="..." #abpBody> also renders the right body with ABP 10.6.0 and Angular 22.1, so the empty body comes from something else on that page. Your single #abpBody version is fine to keep. If you want us to look into the original one, please share a small project that reproduces it.
Thanks
Hi,
The LowCode module keeps its model-change handlers in a static object and doesn't remove them when the application shuts down. Every test starts a new application in the same process, so from the second test on, LowCode calls the handlers of the first application, which is already disposed.
You can remove these handlers when the test application shuts down. Add this to the IdpTestModule in your test project:
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Volo.Abp.LowCode.Modeling;
public override void OnApplicationShutdown(ApplicationShutdownContext context)
{
RemoveLowCodeModelChangedHandlers();
_database?.Dispose();
}
private static void RemoveLowCodeModelChangedHandlers()
{
var manager = DynamicModelManager.Instance;
var handlersField = typeof(DynamicModelManager)
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
.Single(f => f.FieldType == typeof(Func<Task>));
if (handlersField.GetValue(manager) is not Func<Task> handlers)
{
return;
}
foreach (var handler in handlers.GetInvocationList().Cast<Func<Task>>())
{
if (handler.Target != manager)
{
manager.OnModelChanged -= handler;
}
}
}
We've fixed this in the LowCode module, and the fix will be in the next 10.7.x patch release. You can remove this code after upgrading. Your ticket has been refunded.
Thanks
Hi,
checkAccessToken sees a token without a current user and clears the tokens, which reloads the other tabs. Here is an updated workaround. It replaces the storage class from my previous answer.The storage class waits for the worker for up to 3 seconds. If the tokens arrive later and the tab hasn't signed in on its own, it reloads that tab once, or loads the configuration again if it was already reloaded. It also keeps nonce and PKCE_verifier per tab (in 10.1.1 they are in localStorage, so tabs that sign in at the same time overwrite each other's), and it doesn't reload a tab that is still loading the tokens or is in the middle of its own sign-in when another tab gets a token.
import { Injectable, Injector, inject } from '@angular/core';
import { AbpLocalStorageService, ConfigStateService } from '@abp/ng.core';
import { MemoryTokenStorageService } from '@abp/ng.oauth';
// Per-tab sign-in state, so tabs that sign in at the same time don't overwrite each other's nonce.
const sessionKeys = ['nonce', 'PKCE_verifier', 'session_state'];
const reloadKey = 'abp-late-token-reload';
// Reloads this tab once. If it was already reloaded, loads the configuration again instead.
export function reloadOrRefreshConfiguration(injector: Injector) {
if (!sessionStorage.getItem(reloadKey)) {
sessionStorage.setItem(reloadKey, 'true');
location.reload();
return;
}
setTimeout(() => injector.get(ConfigStateService).refreshAppState().subscribe());
}
const hasUser = (injector: Injector) =>
!!injector.get(ConfigStateService).getDeep('currentUser.id');
export function clearReloadMark() {
sessionStorage.removeItem(reloadKey);
}
@Injectable({ providedIn: 'root' })
export class RefreshTokenInMemoryStorageService extends MemoryTokenStorageService {
readonly ready: Promise<void>;
// True when the tab stopped waiting before it got the tokens (timeout or worker error).
timedOut = false;
// True from the start of a sign-in in this tab until it stores its own token. The library
// removes the code from the URL before it exchanges it, so the URL can't be checked later.
signingIn = new URLSearchParams(location.search).has('code');
private hasOwnToken = false;
private waitingForTokens = true;
constructor() {
super();
const storage = this as any;
const injector = inject(Injector);
storage.keysShouldStoreInMemory.push('refresh_token');
inject(AbpLocalStorageService).removeItem('refresh_token');
// When another tab gets a token, don't reload a tab that is still loading the tokens
// (the broadcast fills its cache) or that is finishing its own sign-in.
const checkAuthStateChanges = storage.checkAuthStateChanges;
storage.checkAuthStateChanges = (key: string) => {
if (!this.waitingForTokens && !this.signingIn) {
checkAuthStateChanges(key);
}
};
const port: MessagePort | undefined = storage.port;
if (!storage.useSharedWorker || !port) {
this.waitingForTokens = false;
this.ready = Promise.resolve();
return;
}
let done = false;
let lateTokens: 'apply' | 'ignore' | undefined;
this.ready = new Promise<void>(resolve => {
const finish = () => {
done = true;
this.waitingForTokens = false;
resolve();
};
const stopWaiting = () => {
if (!done) {
this.timedOut = true;
finish();
}
};
const handler = port.onmessage!;
port.onmessage = event => {
const isLate = done && this.timedOut && event.data?.action === 'get';
if (isLate) {
// A tab that signs in on its own keeps its own tokens.
lateTokens ??= this.hasOwnToken || this.signingIn ? 'ignore' : 'apply';
if (lateTokens === 'ignore') {
return;
}
}
// A get answer is a snapshot from when it was asked. Don't let it overwrite
// a newer value that this tab already stored or got from another tab.
if (event.data?.action !== 'get' || !storage.cache.has(event.data.key)) {
handler.call(port, event);
}
// The worker answers in order, so this is the last reply.
if (event.data?.action === 'get' && event.data.key === 'refresh_token') {
if (isLate && this.getItem('access_token') && !hasUser(injector)) {
// The tab started without the tokens of the other tabs.
reloadOrRefreshConfiguration(injector);
}
finish();
}
};
storage.worker?.addEventListener('error', stopWaiting);
setTimeout(stopWaiting, 3000);
});
port.postMessage({ action: 'get', key: 'refresh_token' });
}
override getItem(key: string): string | null {
return sessionKeys.includes(key) ? sessionStorage.getItem(key) : super.getItem(key);
}
override setItem(key: string, value: string): void {
if (key === 'access_token') {
this.signingIn = false;
this.hasOwnToken = true;
}
if (sessionKeys.includes(key)) {
sessionStorage.setItem(key, value);
} else {
super.setItem(key, value);
}
}
override removeItem(key: string): void {
if (sessionKeys.includes(key)) {
sessionStorage.removeItem(key);
} else {
super.removeItem(key);
}
}
}
AuthService.init() waits for it. The route guard and the 401 handler below both call navigateToLogin(), so it keeps the first call, runs it after init(), and skips it if the tab got a token in the meantime:
import { Injectable, inject } from '@angular/core';
import { Params } from '@angular/router';
import { AbpOAuthService } from '@abp/ng.oauth';
import { RefreshTokenInMemoryStorageService } from './refresh-token-in-memory-storage.service';
@Injectable({ providedIn: 'root' })
export class WaitForTokensOAuthService extends AbpOAuthService {
private tokenStorage = inject(RefreshTokenInMemoryStorageService);
private initDone!: () => void;
// Completes when init() finishes, also when a 401 arrives before init() is called.
private initialized = new Promise<void>(resolve => (this.initDone = resolve));
private signingIn = false;
override async init() {
try {
await this.tokenStorage.ready;
return await super.init();
} finally {
this.initDone();
}
}
// The route guard and the 401 handler can both call this, keep the first call
// and run it after init().
override navigateToLogin(queryParams?: Params) {
if (this.signingIn) {
return;
}
this.signingIn = true;
this.tokenStorage.signingIn = true;
this.initialized.then(() => {
// The sign-in callback can finish while this call waits.
if (this.isAuthenticated) {
this.signingIn = false;
this.tokenStorage.signingIn = false;
return;
}
try {
super.navigateToLogin(queryParams);
} catch (error) {
this.signingIn = false;
this.tokenStorage.signingIn = false;
throw error;
}
});
}
}
This check keeps the default behavior when the worker answered in time. When the token arrived during the configuration request, it reloads only that tab instead of clearing the tokens of every tab:
import { Injector } from '@angular/core';
import { CheckAuthenticationStateFn, ConfigStateService } from '@abp/ng.core';
import { checkAccessToken } from '@abp/ng.oauth';
import { OAuthService } from 'angular-oauth2-oidc';
import {
RefreshTokenInMemoryStorageService,
clearReloadMark,
reloadOrRefreshConfiguration,
} from './refresh-token-in-memory-storage.service';
export const checkAccessTokenOrReload: CheckAuthenticationStateFn = (injector: Injector) => {
if (!injector.get(RefreshTokenInMemoryStorageService).timedOut) {
// The tokens were loaded before the configuration request, keep the default check.
clearReloadMark();
checkAccessToken(injector);
return;
}
const hasValidToken = injector.get(OAuthService).hasValidAccessToken();
const hasUser = !!injector.get(ConfigStateService).getDeep('currentUser.id');
if (hasValidToken && !hasUser) {
// The token arrived during the configuration request.
reloadOrRefreshConfiguration(injector);
}
};
When the tab has no token, for example after a reload of the only tab, this 401 handler starts the sign-in with the current URL instead of signing out and going to /:
import { Injectable } from '@angular/core';
import { AbpAuthenticationErrorHandler } from '@abp/ng.theme.shared';
@Injectable({ providedIn: 'root' })
export class SignInAgainErrorHandler extends AbpAuthenticationErrorHandler {
override execute() {
if (this.authService.getAccessToken()) {
super.execute();
return;
}
// No token in this tab (for example after reloading the only tab): sign in and come back here.
this.authService.navigateToLogin({
returnUrl: location.pathname + location.search + location.hash,
});
}
}
Register them after provideAbpOAuth():
import { AuthService, CHECK_AUTHENTICATION_STATE_FN_KEY } from '@abp/ng.core';
import { MemoryTokenStorageService, provideAbpOAuth } from '@abp/ng.oauth';
import { AbpAuthenticationErrorHandler } from '@abp/ng.theme.shared';
import { RefreshTokenInMemoryStorageService } from './refresh-token-in-memory-storage.service';
import { WaitForTokensOAuthService } from './wait-for-tokens-oauth.service';
import { checkAccessTokenOrReload } from './check-access-token-or-reload';
import { SignInAgainErrorHandler } from './sign-in-again-error-handler';
providers: [
// ...
provideAbpOAuth(),
{ provide: MemoryTokenStorageService, useExisting: RefreshTokenInMemoryStorageService },
{ provide: AuthService, useExisting: WaitForTokensOAuthService },
{ provide: CHECK_AUTHENTICATION_STATE_FN_KEY, useValue: checkAccessTokenOrReload },
{ provide: AbpAuthenticationErrorHandler, useExisting: SignInAgainErrorHandler },
// ...
]
A few notes:
OAuthStorage, and allow data: in worker-src. If the CSP blocks the worker, each tab signs in and signs out on its own.RestService (or the generated proxies) without a token goes to the login page.MemoryTokenStorageService, so it's a temporary workaround.This doesn't change the refresh timers. Each tab still refreshes on its own, so with a zero reuse leeway two tabs can send the same refresh token. When the second one fails, the tokens are cleared in every tab and the tabs sign in again when a guard or a protected call needs it, so a tab can lose its current page. A reuse leeway accepts the same refresh token again only within that window (for example 10 seconds), which covers tabs that refresh at the same time.
When the only tab is reloaded, the browser can end the SharedWorker and then the tokens are lost. If the route uses authGuard, or the page calls a protected API through RestService, the app signs in again with the current URL and the user comes back to the same page. No password is needed while the AuthServer cookie is valid. Public pages without either stay anonymous:
import { authGuard, permissionGuard } from '@abp/ng.core';
{
path: 'my-tasks',
canActivate: [authGuard, permissionGuard],
// ...
}
@abp/ng.oauth, so you can remove this workaround later.Thanks
Hi,
Your ABP license already includes a Blazorise PRO license, so you can generate the Product Token yourself. Please follow the steps here:
https://abp.io/support/questions/9912/How-to-get-and-use-Blazorise-License-in-ABP-Projects
If your solution has a .Client project, set options.ProductToken in the AddBlazorise call of the *BlazorClientModule as well. Otherwise the banner will come back after the client side loads.
Thanks