Browse Source

Merge pull request #18418 from abpframework/issue/18405

User can set dynamic layouts via the inject token
pull/18432/head
Masum ULU 2 years ago
committed by GitHub
parent
commit
66bda6ee11
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts
  2. 2
      npm/ng-packs/packages/components/extensible/src/lib/enums/components.ts
  3. 109
      npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
  4. 7
      npm/ng-packs/packages/core/src/lib/constants/default-layouts.ts
  5. 1
      npm/ng-packs/packages/core/src/lib/constants/index.ts
  6. 6
      npm/ng-packs/packages/core/src/lib/core.module.ts
  7. 5
      npm/ng-packs/packages/core/src/lib/enums/common.ts
  8. 1
      npm/ng-packs/packages/core/src/lib/models/common.ts
  9. 3
      npm/ng-packs/packages/core/src/lib/tokens/dynamic-layout.token.ts
  10. 3
      templates/app/angular/src/app/app.module.ts

60
npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts

@ -1,5 +1,5 @@
import { EXTENSIONS_FORM_PROP, EXTENSIONS_FORM_PROP_DATA } from './../../tokens/extensions.token';
import { ABP, LocalizationModule, PermissionDirective, ShowPasswordDirective, TrackByService } from '@abp/ng.core';
import {EXTENSIONS_FORM_PROP, EXTENSIONS_FORM_PROP_DATA} from './../../tokens/extensions.token';
import {ABP, LocalizationModule, PermissionDirective, ShowPasswordDirective, TrackByService} from '@abp/ng.core';
import {
AfterViewInit,
ChangeDetectionStrategy,
@ -28,20 +28,20 @@ import {
NgbTimepickerModule,
NgbTypeaheadModule,
} from '@ng-bootstrap/ng-bootstrap';
import { Observable, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { DateAdapter, DisabledDirective, TimeAdapter } from '@abp/ng.theme.shared';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
import { FormProp } from '../../models/form-props';
import { PropData } from '../../models/props';
import { selfFactory } from '../../utils/factory.util';
import { addTypeaheadTextSuffix } from '../../utils/typeahead.util';
import { eThemeSharedComponents } from '../../enums/components';
import { ExtensibleDateTimePickerComponent } from '../date-time-picker/extensible-date-time-picker.component';
import { NgxValidateCoreModule } from '@ngx-validate/core';
import { ExtensibleFormPropService } from '../../services/extensible-form-prop.service';
import { CreateInjectorPipe } from '../../pipes/create-injector.pipe';
import { AsyncPipe, NgClass, NgSwitch, NgSwitchCase, NgTemplateOutlet } from '@angular/common';
import {Observable, of} from 'rxjs';
import {debounceTime, distinctUntilChanged, switchMap} from 'rxjs/operators';
import {DateAdapter, DisabledDirective, TimeAdapter} from '@abp/ng.theme.shared';
import {EXTRA_PROPERTIES_KEY} from '../../constants/extra-properties';
import {FormProp} from '../../models/form-props';
import {PropData} from '../../models/props';
import {selfFactory} from '../../utils/factory.util';
import {addTypeaheadTextSuffix} from '../../utils/typeahead.util';
import {eExtensibleComponents} from '../../enums/components';
import {ExtensibleDateTimePickerComponent} from '../date-time-picker/extensible-date-time-picker.component';
import {NgxValidateCoreModule} from '@ngx-validate/core';
import {ExtensibleFormPropService} from '../../services/extensible-form-prop.service';
import {CreateInjectorPipe} from '../../pipes/create-injector.pipe';
import {AsyncPipe, NgClass, NgSwitch, NgSwitchCase, NgTemplateOutlet} from '@angular/common';
@Component({
selector: 'abp-extensible-form-prop',
@ -73,8 +73,8 @@ import { AsyncPipe, NgClass, NgSwitch, NgSwitchCase, NgTemplateOutlet } from '@a
useFactory: selfFactory,
deps: [[new Optional(), new SkipSelf(), ControlContainer]],
},
{ provide: NgbDateAdapter, useClass: DateAdapter },
{ provide: NgbTimeAdapter, useClass: TimeAdapter },
{provide: NgbDateAdapter, useClass: DateAdapter},
{provide: NgbTimeAdapter, useClass: TimeAdapter},
],
})
export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
@ -98,7 +98,7 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
validators: ValidatorFn[] = [];
readonly!: boolean;
typeaheadModel: any;
passwordKey = eThemeSharedComponents.PasswordComponent;
passwordKey = eExtensibleComponents.PasswordComponent;
disabledFn = (data: PropData) => false;
@ -107,8 +107,8 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
}
setTypeaheadValue(selectedOption: ABP.Option<string>) {
this.typeaheadModel = selectedOption || { key: null, value: null };
const { key, value } = this.typeaheadModel;
this.typeaheadModel = selectedOption || {key: null, value: null};
const {key, value} = this.typeaheadModel;
const [keyControl, valueControl] = this.getTypeaheadControls();
if (valueControl?.value && !value) valueControl.markAsDirty();
keyControl?.setValue(key);
@ -118,10 +118,10 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
search = (text$: Observable<string>) =>
text$
? text$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(text => this.prop?.options?.(this.data, text) || of([])),
)
debounceTime(300),
distinctUntilChanged(),
switchMap(text => this.prop?.options?.(this.data, text) || of([])),
)
: of([]);
typeaheadFormatter = (option: ABP.Option<any>) => option.key;
@ -134,7 +134,7 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
}
private getTypeaheadControls() {
const { name } = this.prop;
const {name} = this.prop;
const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`;
const keyControl =
this.form.get(addTypeaheadTextSuffix(extraPropName)) ||
@ -162,9 +162,9 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
return this.service.getType(prop);
}
ngOnChanges({ prop, data }: SimpleChanges) {
ngOnChanges({prop, data}: SimpleChanges) {
const currentProp = prop?.currentValue as FormProp;
const { options, readonly, disabled, validators, className, template } = currentProp || {};
const {options, readonly, disabled, validators, className, template} = currentProp || {};
if (template) {
this.injectorForCustomComponent = Injector.create({
providers: [
@ -176,7 +176,7 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
provide: EXTENSIONS_FORM_PROP_DATA,
useValue: (data?.currentValue as PropData)?.record,
},
{ provide: ControlContainer, useExisting: FormGroupDirective },
{provide: ControlContainer, useExisting: FormGroupDirective},
],
parent: this.injector,
});
@ -198,6 +198,6 @@ export class ExtensibleFormPropComponent implements OnChanges, AfterViewInit {
const [keyControl, valueControl] = this.getTypeaheadControls();
if (keyControl && valueControl)
this.typeaheadModel = { key: keyControl.value, value: valueControl.value };
this.typeaheadModel = {key: keyControl.value, value: valueControl.value};
}
}

2
npm/ng-packs/packages/components/extensible/src/lib/enums/components.ts

@ -1,3 +1,3 @@
export const enum eThemeSharedComponents {
export const enum eExtensibleComponents {
PasswordComponent = 'ThemeShared.Extensions.PasswordComponent',
}

109
npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts

@ -1,60 +1,59 @@
import { Component, Injector, isDevMode, OnInit, Optional, SkipSelf, Type } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { eLayoutType } from '../enums/common';
import { ABP } from '../models';
import { ReplaceableComponents } from '../models/replaceable-components';
import { LocalizationService } from '../services/localization.service';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
import { RouterEvents } from '../services/router-events.service';
import { RoutesService } from '../services/routes.service';
import { SubscriptionService } from '../services/subscription.service';
import { findRoute, getRoutePath } from '../utils/route-utils';
import { TreeNode } from '../utils/tree-utils';
import {
Component,
inject,
isDevMode,
OnInit,
Optional,
SkipSelf,
Type
} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {eLayoutType} from '../enums/common';
import {ABP} from '../models';
import {ReplaceableComponents} from '../models/replaceable-components';
import {LocalizationService} from '../services/localization.service';
import {ReplaceableComponentsService} from '../services/replaceable-components.service';
import {RouterEvents} from '../services/router-events.service';
import {RoutesService} from '../services/routes.service';
import {SubscriptionService} from '../services/subscription.service';
import {findRoute, getRoutePath} from '../utils/route-utils';
import {TreeNode} from '../utils/tree-utils';
import {DYNAMIC_LAYOUTS_TOKEN} from "../tokens/dynamic-layout.token";
@Component({
selector: 'abp-dynamic-layout',
template: ` <ng-container *ngIf="isLayoutVisible" [ngComponentOutlet]="layout"></ng-container> `,
template: `
<ng-container *ngIf="isLayoutVisible" [ngComponentOutlet]="layout"></ng-container> `,
providers: [SubscriptionService],
})
export class DynamicLayoutComponent implements OnInit {
layout?: Type<any>;
layoutKey?: eLayoutType;
// TODO: Consider a shared enum (eThemeSharedComponents) for known layouts
readonly layouts = new Map([
['application', 'Theme.ApplicationLayoutComponent'],
['account', 'Theme.AccountLayoutComponent'],
['empty', 'Theme.EmptyLayoutComponent'],
]);
readonly layouts = inject(DYNAMIC_LAYOUTS_TOKEN)
isLayoutVisible = true;
private router!: Router;
private route!: ActivatedRoute;
private routes!: RoutesService;
private readonly router = inject(Router);
private readonly route = inject(ActivatedRoute);
private readonly routes = inject(RoutesService);
private localizationService = inject(LocalizationService)
private replaceableComponents = inject(ReplaceableComponentsService)
private subscription = inject(SubscriptionService)
private routerEvents = inject(RouterEvents)
constructor(
injector: Injector,
private localizationService: LocalizationService,
private replaceableComponents: ReplaceableComponentsService,
private subscription: SubscriptionService,
private routerEvents: RouterEvents,
@Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent,
) {
if (dynamicLayoutComponent) {
if (isDevMode()) console.warn('DynamicLayoutComponent must be used only in AppComponent.');
return;
}
this.route = injector.get(ActivatedRoute);
this.router = injector.get(Router);
this.routes = injector.get(RoutesService);
this.checkLayoutOnNavigationEnd();
this.listenToLanguageChange();
}
ngOnInit(): void {
if(this.layout){
if (this.layout) {
return;
}
this.getLayout()
@ -65,22 +64,10 @@ export class DynamicLayoutComponent implements OnInit {
this.subscription.addOne(navigationEnd$, () => this.getLayout());
}
private getLayout() {
let expectedLayout = (this.route.snapshot.data || {}).layout;
if (!expectedLayout) {
let node = findRoute(this.routes, getRoutePath(this.router));
node = { parent: node } as TreeNode<ABP.Route>;
while (node.parent) {
node = node.parent;
private getLayout() {
let expectedLayout = this.getExtractedLayout();
if (node.layout) {
expectedLayout = node.layout;
break;
}
}
}
if (!expectedLayout) expectedLayout = eLayoutType.empty;
@ -91,14 +78,36 @@ export class DynamicLayoutComponent implements OnInit {
this.layout = this.getComponent(key)?.component;
this.layoutKey = expectedLayout;
}
if(!this.layout){
if (!this.layout) {
this.showLayoutNotFoundError(expectedLayout);
}
}
private getExtractedLayout() {
const routeData = (this.route.snapshot.data || {});
let expectedLayout = routeData['layout'] as eLayoutType;
if (expectedLayout) {
return expectedLayout;
}
let node = findRoute(this.routes, getRoutePath(this.router));
node = {parent: node} as TreeNode<ABP.Route>;
while (node.parent) {
node = node.parent;
if (node.layout) {
expectedLayout = node.layout;
break;
}
}
return expectedLayout;
}
showLayoutNotFoundError(layoutName: string) {
let message = `Layout ${layoutName} not found.`;
if(layoutName === 'account'){
if (layoutName === 'account') {
message = 'Account layout not found. Please check your configuration. If you are using LeptonX, please make sure you have added "AccountLayoutModule.forRoot()" to your app.module configuration.';
}
console.warn(message);

7
npm/ng-packs/packages/core/src/lib/constants/default-layouts.ts

@ -0,0 +1,7 @@
import {eLayoutType, eThemeSharedComponents} from "../enums";
export const DEFAULT_DYNAMIC_LAYOUTS = new Map<string, string>([
[eLayoutType.application, eThemeSharedComponents.ApplicationLayoutComponent],
[eLayoutType.account, eThemeSharedComponents.AccountLayoutComponent],
[eLayoutType.empty, eThemeSharedComponents.EmptyLayoutComponent],
]);

1
npm/ng-packs/packages/core/src/lib/constants/index.ts

@ -1 +1,2 @@
export * from './different-locales';
export * from './default-layouts';

6
npm/ng-packs/packages/core/src/lib/core.module.ts

@ -39,6 +39,8 @@ import { QUEUE_MANAGER } from './tokens/queue.token';
import { DefaultQueueManager } from './utils/queue';
import { IncludeLocalizationResourcesProvider } from './providers/include-localization-resources.provider';
import { SORT_COMPARE_FUNC, compareFuncFactory } from './tokens/compare-func.token';
import {DYNAMIC_LAYOUTS_TOKEN} from "./tokens/dynamic-layout.token";
import {DEFAULT_DYNAMIC_LAYOUTS} from "./constants";
const standaloneDirectives = [
AutofocusDirective,
@ -186,6 +188,10 @@ export class CoreModule {
useValue: options.othersGroup || 'AbpUi::OthersGroup',
},
IncludeLocalizationResourcesProvider,
{
provide: DYNAMIC_LAYOUTS_TOKEN,
useValue: options.dynamicLayouts || DEFAULT_DYNAMIC_LAYOUTS
}
],
};
}

5
npm/ng-packs/packages/core/src/lib/enums/common.ts

@ -3,3 +3,8 @@ export const enum eLayoutType {
application = 'application',
empty = 'empty',
}
export const enum eThemeSharedComponents {
ApplicationLayoutComponent = 'Theme.ApplicationLayoutComponent',
AccountLayoutComponent = 'Theme.AccountLayoutComponent',
EmptyLayoutComponent = 'Theme.EmptyLayoutComponent',
}

1
npm/ng-packs/packages/core/src/lib/models/common.ts

@ -13,6 +13,7 @@ export namespace ABP {
tenantKey?: string;
localizations?: Localization[];
othersGroup?: string;
dynamicLayouts?: Map<string, string>;
}
export interface Child {

3
npm/ng-packs/packages/core/src/lib/tokens/dynamic-layout.token.ts

@ -0,0 +1,3 @@
import {InjectionToken} from "@angular/core";
export const DYNAMIC_LAYOUTS_TOKEN = new InjectionToken<Map<string, string>>('DYNAMIC_LAYOUTS')

3
templates/app/angular/src/app/app.module.ts

@ -16,7 +16,7 @@ import { AppComponent } from './app.component';
import { APP_ROUTE_PROVIDER } from './route.provider';
import { FeatureManagementModule } from '@abp/ng.feature-management';
import { AbpOAuthModule } from '@abp/ng.oauth';
import { AccountLayoutModule } from '@abp/ng.theme.lepton-x/account';
@NgModule({
imports: [
BrowserModule,
@ -28,6 +28,7 @@ import { AbpOAuthModule } from '@abp/ng.oauth';
}),
AbpOAuthModule.forRoot(),
ThemeSharedModule.forRoot(),
AccountLayoutModule.forRoot(),
AccountConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
TenantManagementConfigModule.forRoot(),

Loading…
Cancel
Save