Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular UI: Created the UserMenuService to get user menu items dynamically #11214

Merged
merged 12 commits into from
Jan 7, 2022
67 changes: 60 additions & 7 deletions docs/en/UI/Angular/Theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,25 +204,78 @@ constructor(private sessionState: SessionStateService) {
}
````



##### User Menu

User menu is a component that can be replaceable. See an example to learn how can you replace it:
`UserMenuService` is used to get the user menu's items and render on the dropdown. You can add/update/remove an item by using the service.

You can either pass a `component`, a piece of `html` or a `textTemplate` to render an item.
All of the options are shown below. You can choose either of them.

**Example: Adding/updating/removing an user menu item**

````ts
import { eThemeBasicComponents } from '@abp/ng.theme.basic';
import { NavItemsService } from '@abp/ng.theme.shared';
import { eUserMenuItems } from '@abp/ng.theme.basic';
import { UserMenuService } from '@abp/ng.theme.shared';
import { Component } from '@angular/core';
import { Router } from '@angular/router';

// make sure that you import this component in a NgModule
@Component({
selector: 'abp-current-user-test',
template: `
<a class="dropdown-item pointer" (click)="data.action()">
<i *ngIf="data.textTemplate.icon" [class]="data.textTemplate.icon"></i>
{{ data.textTemplate.text | abpLocalization }}
</a>
`,
})
export class UserMenuItemComponent {
// you can inject the data through `INJECTOR_PIPE_DATA_TOKEN` token
constructor(@Inject(INJECTOR_PIPE_DATA_TOKEN) public data: UserMenu) {}
}

@Component({/* component metadata */})
export class AppComponent {
constructor(private navItems: NavItemsService) {
this.navItems.patchItem(eThemeBasicComponents.CurrentUser, { component: MyUserMenuComponent });
constructor(private userMenu: UserMenuService, private router: Router) {
this.userMenu.addItems([
{
id: 'UserMenu.MyAccount',
order: 1,

// HTML example
html: `<a class="dropdown-item pointer">My account</a>`,

// text template example
textTemplate: {
text: 'AbpAccount::MyAccount',
icon: 'fa fa-cog',
},
// component example
component: UserMenuItemComponent,

action: () => {
this.router.navigateByUrl('/account/manage');
},
},
]);

this.userMenu.patchItem(eUserMenuItems.MyAccount, {
html: `<a class="dropdown-item pointer">My profile</a>`,
});

this.userMenu.removeItem(eUserMenuItems.Logout);
}
}
````

[`ConfigStateService`](Config-State-Service.md) service can be used to obtain the `application-configuration` API response (e.g. getting current user or tenant).
In the example above, added a new user menu item labeled "Reports", updated the "My account" item HTML content, and removed the "Logout" item.

See the result:

![image](https://user-images.githubusercontent.com/34455572/148387770-5b5e25fb-f855-447c-8ead-c04c69b6d6f7.png)

#### Page Alerts

`PageAlertService` service is used to get the current page alerts to render on the layout. See the [Page Alerts](Page-Alerts.md) document to learn more.
`PageAlertService` service is used to get the current page alerts to render on the layout. See the [Page Alerts](Page-Alerts.md) document to learn more.
3 changes: 3 additions & 0 deletions npm/ng-packs/packages/core/src/lib/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { LocalizationModule } from './localization.module';
import { ABP } from './models/common';
import { LocalizationPipe } from './pipes/localization.pipe';
import { SortPipe } from './pipes/sort.pipe';
import { ToInjectorPipe } from './pipes/to-injector.pipe';
import { CookieLanguageProvider } from './providers/cookie-language.provider';
import { LocaleProvider } from './providers/locale.provider';
import { LocalizationService } from './services/localization.service';
Expand Down Expand Up @@ -65,6 +66,7 @@ export function storageFactory(): OAuthStorage {
RouterOutletComponent,
SortPipe,
StopPropagationDirective,
ToInjectorPipe,
],
imports: [
OAuthModule,
Expand All @@ -89,6 +91,7 @@ export function storageFactory(): OAuthStorage {
RouterOutletComponent,
SortPipe,
StopPropagationDirective,
ToInjectorPipe,
],
providers: [LocalizationPipe],
entryComponents: [
Expand Down
1 change: 1 addition & 0 deletions npm/ng-packs/packages/core/src/lib/pipes/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './localization.pipe';
export * from './sort.pipe';
export * from './to-injector.pipe';
28 changes: 28 additions & 0 deletions npm/ng-packs/packages/core/src/lib/pipes/to-injector.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { InjectionToken, Injector, Pipe, PipeTransform } from '@angular/core';

export const INJECTOR_PIPE_DATA_TOKEN = new InjectionToken<PipeTransform>(
'INJECTOR_PIPE_DATA_TOKEN',
);

@Pipe({
name: 'toInjector',
})
export class ToInjectorPipe implements PipeTransform {
constructor(private injector: Injector) {}
transform(
value: any,
token: InjectionToken<any> = INJECTOR_PIPE_DATA_TOKEN,
name = 'ToInjectorPipe',
): Injector {
return Injector.create({
providers: [
{
provide: token,
useValue: value,
},
],
parent: this.injector,
name,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export * from './logo/logo.component';
export * from './nav-items/current-user.component';
export * from './nav-items/languages.component';
export * from './nav-items/nav-items.component';
export * from './page-alert-container/page-alert-container.component';
export * from './routes/routes.component';
export * from './validation-error/validation-error.component';
export * from './page-alert-container/page-alert-container.component';
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,35 @@
aria-labelledby="dropdownMenuLink"
[class.d-block]="smallScreen && currentUserDropdown.isOpen()"
>
<a class="dropdown-item pointer" (click)="navigateToManageProfile()"
><i class="fa fa-cog me-1"></i>{{ 'AbpAccount::MyAccount' | abpLocalization }}</a
>
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()"
><i class="fa fa-power-off me-1"></i>{{ 'AbpUi::Logout' | abpLocalization }}</a
>
<ng-container *ngFor="let item of userMenu.items$ | async; trackBy: trackByFn">
<ng-container *ngIf="item.visible && item.visible()">
<li class="nav-item d-flex align-items-center" *abpPermission="item.requiredPolicy">
<ng-container
*ngIf="item.component; else htmlTemplate"
[ngComponentOutlet]="item.component"
[ngComponentOutletInjector]="item | toInjector"
></ng-container>

<ng-template #htmlTemplate>
<div
*ngIf="item.html; else textTemplate"
[innerHTML]="item.html"
(click)="item.action ? item.action() : null"
></div>
</ng-template>

<ng-template #textTemplate>
<a
*ngIf="item.textTemplate"
(click)="item.action ? item.action() : null"
class="dropdown-item pointer"
>
<i *ngIf="item.textTemplate.icon" class="me-1 {{ item.textTemplate.icon }}"></i>
{{ item.textTemplate.text | abpLocalization }}</a
>
</ng-template>
</li>
</ng-container>
</ng-container>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
NAVIGATE_TO_MANAGE_PROFILE,
SessionStateService,
} from '@abp/ng.core';
import { Component, Inject } from '@angular/core';
import { UserMenu, UserMenuService } from '@abp/ng.theme.shared';
import { Component, Inject, TrackByFunction } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
Expand All @@ -16,12 +17,15 @@ export class CurrentUserComponent {
currentUser$: Observable<CurrentUserDto> = this.configState.getOne$('currentUser');
selectedTenant$ = this.sessionState.getTenant$();

trackByFn: TrackByFunction<UserMenu> = (_, element) => element.id;

get smallScreen(): boolean {
return window.innerWidth < 992;
}

constructor(
@Inject(NAVIGATE_TO_MANAGE_PROFILE) public navigateToManageProfile: () => void,
@Inject(NAVIGATE_TO_MANAGE_PROFILE) public readonly navigateToManageProfile: () => void,
public readonly userMenu: UserMenuService,
private authService: AuthService,
private configState: ConfigStateService,
private sessionState: SessionStateService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<ng-container
*ngIf="item.component; else htmlTemplate"
[ngComponentOutlet]="item.component"
[ngComponentOutletInjector]="item | toInjector"
></ng-container>

<ng-template #htmlTemplate>
Expand Down
1 change: 1 addition & 0 deletions npm/ng-packs/packages/theme-basic/src/lib/enums/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './components';
export * from './user-menu-items';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const enum eUserMenuItems {
MyAccount = 'UserMenu.MyAccount',
Logout = 'UserMenu.Logout',
}
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './nav-item.provider';
export * from './styles.provider';
export * from './user-menu.provider';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AuthService, NAVIGATE_TO_MANAGE_PROFILE } from '@abp/ng.core';
import { UserMenuService } from '@abp/ng.theme.shared';
import { APP_INITIALIZER, Injector } from '@angular/core';
import { eUserMenuItems } from '../enums/user-menu-items';

export const BASIC_THEME_USER_MENU_PROVIDERS = [
{
provide: APP_INITIALIZER,
useFactory: configureUserMenu,
deps: [Injector],
multi: true,
},
];

export function configureUserMenu(injector: Injector) {
const userMenu = injector.get(UserMenuService);
const authService = injector.get(AuthService);
const navigateToManageProfile = injector.get(NAVIGATE_TO_MANAGE_PROFILE);

return () => {
userMenu.addItems([
{
id: eUserMenuItems.MyAccount,
order: 100,
textTemplate: {
text: 'AbpAccount::MyAccount',
icon: 'fa fa-cog',
},
action: () => navigateToManageProfile(),
},
{
id: eUserMenuItems.Logout,
order: 101,
textTemplate: {
text: 'AbpUi::Logout',
icon: 'fa fa-power-off',
},
action: () => {
authService.logout().subscribe();
},
},
]);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ValidationErrorComponent } from './components/validation-error/validati
import { LazyStyleHandler } from './handlers/lazy-style.handler';
import { BASIC_THEME_NAV_ITEM_PROVIDERS } from './providers/nav-item.provider';
import { BASIC_THEME_STYLES_PROVIDERS } from './providers/styles.provider';
import { BASIC_THEME_USER_MENU_PROVIDERS } from './providers/user-menu.provider';

export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, EmptyLayoutComponent];

Expand Down Expand Up @@ -70,6 +71,7 @@ export class ThemeBasicModule {
ngModule: ThemeBasicModule,
providers: [
BASIC_THEME_NAV_ITEM_PROVIDERS,
BASIC_THEME_USER_MENU_PROVIDERS,
BASIC_THEME_STYLES_PROVIDERS,
{
provide: VALIDATION_ERROR_TEMPLATE,
Expand Down
3 changes: 2 additions & 1 deletion npm/ng-packs/packages/theme-shared/src/lib/models/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './common';
export * from './confirmation';
export * from './nav-item';
export * from './statistics';
export * from './toaster';
export * from './nav-item';
export * from './user-menu';
10 changes: 10 additions & 0 deletions npm/ng-packs/packages/theme-shared/src/lib/models/user-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NavItem } from './nav-item';

export class UserMenu extends NavItem {
textTemplate?: UserMenuTextTemplate;
}

export interface UserMenuTextTemplate {
text: string;
icon?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { BehaviorSubject, Observable } from 'rxjs';
import { NavItem } from '../models/nav-item';

export abstract class AbstractMenuService<T extends NavItem> {
protected abstract baseClass;

protected _items$ = new BehaviorSubject<T[]>([]);

get items(): T[] {
return this._items$.value;
}

get items$(): Observable<T[]> {
return this._items$.asObservable();
}

addItems(newItems: T[]) {
const items = [...this.items];
newItems.forEach(item => {
const index = items.findIndex(i => i.id === item.id);
const data = new this.baseClass(item);

if (index > -1) {
items[index] = data;
return;
}

items.push(data);
});
items.sort(this.sortItems);
this._items$.next(items);
}

removeItem(id: string | number) {
const index = this.items.findIndex(item => item.id === id);

if (index < 0) return;

const items = [...this.items.slice(0, index), ...this.items.slice(index + 1)];
this._items$.next(items);
}

patchItem(id: string | number, item: Partial<Omit<T, 'id'>>) {
const index = this.items.findIndex(i => i.id === id);

if (index < 0) return;

const items = [...this.items];
items[index] = new this.baseClass({ ...items[index], ...item });
items.sort(this.sortItems);
this._items$.next(items);
}

private sortItems(a: T, b: T) {
if (!a.order) return 1;
if (!b.order) return -1;

return a.order - b.order;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './confirmation.service';
export * from './nav-items.service';
export * from './page-alert.service';
export * from './toaster.service';
export * from './user-menu.service';
Loading