mirror of https://github.com/abpframework/abp.git
35 changed files with 521 additions and 60 deletions
@ -1,23 +0,0 @@ |
|||
import { Directive, ElementRef, EventEmitter, Output, OnDestroy } from '@angular/core'; |
|||
import { fromEvent } from 'rxjs'; |
|||
import { debounceTime, filter, tap } from 'rxjs/operators'; |
|||
import { takeUntilDestroy } from '../utils/rxjs-utils'; |
|||
|
|||
@Directive({ |
|||
selector: 'form[ngSubmit]', |
|||
}) |
|||
export class FormKeyupListenerDirective implements OnDestroy { |
|||
@Output() ngSubmit = new EventEmitter(); |
|||
|
|||
constructor(private elRef: ElementRef) { |
|||
fromEvent(elRef.nativeElement as HTMLElement, 'keyup') |
|||
.pipe( |
|||
debounceTime(200), |
|||
filter((key: KeyboardEvent) => key && key.key === 'Enter'), |
|||
takeUntilDestroy(this), |
|||
) |
|||
.subscribe(() => this.ngSubmit.emit()); |
|||
} |
|||
|
|||
ngOnDestroy(): void {} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
import { |
|||
Directive, |
|||
ElementRef, |
|||
OnDestroy, |
|||
OnInit, |
|||
Self, |
|||
ChangeDetectorRef, |
|||
HostBinding, |
|||
Input, |
|||
Output, |
|||
EventEmitter, |
|||
} from '@angular/core'; |
|||
import { FormGroupDirective, FormGroup, FormControl, ɵNgNoValidate } from '@angular/forms'; |
|||
import { fromEvent } from 'rxjs'; |
|||
import { takeUntilDestroy } from '../utils'; |
|||
import { debounceTime, filter } from 'rxjs/operators'; |
|||
|
|||
type Controls = { [key: string]: FormControl } | FormGroup[]; |
|||
|
|||
@Directive({ |
|||
selector: 'form[ngSubmit][formGroup]', |
|||
}) |
|||
export class FormSubmitDirective implements OnInit, OnDestroy { |
|||
@Input() |
|||
notValidateOnSubmit: string | boolean; |
|||
|
|||
constructor( |
|||
@Self() private formGroupDirective: FormGroupDirective, |
|||
private host: ElementRef<HTMLFormElement>, |
|||
private cdRef: ChangeDetectorRef, |
|||
) {} |
|||
|
|||
ngOnInit() { |
|||
fromEvent(this.host.nativeElement as HTMLElement, 'keyup') |
|||
.pipe( |
|||
debounceTime(200), |
|||
filter((key: KeyboardEvent) => key && key.key === 'Enter'), |
|||
takeUntilDestroy(this), |
|||
) |
|||
.subscribe(() => { |
|||
this.host.nativeElement.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); |
|||
}); |
|||
|
|||
fromEvent(this.host.nativeElement, 'submit') |
|||
.pipe( |
|||
takeUntilDestroy(this), |
|||
filter(() => !this.notValidateOnSubmit && typeof this.notValidateOnSubmit !== 'string'), |
|||
) |
|||
.subscribe(() => { |
|||
const { form } = this.formGroupDirective; |
|||
|
|||
setDirty(form.controls as { [key: string]: FormControl }); |
|||
form.markAsDirty(); |
|||
|
|||
this.cdRef.detectChanges(); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy(): void {} |
|||
} |
|||
|
|||
function setDirty(controls: Controls) { |
|||
if (Array.isArray(controls)) { |
|||
controls.forEach(group => { |
|||
setDirty(group.controls as { [key: string]: FormControl }); |
|||
}); |
|||
return; |
|||
} |
|||
|
|||
Object.keys(controls).forEach(key => { |
|||
controls[key].markAsDirty(); |
|||
controls[key].updateValueAndValidity(); |
|||
}); |
|||
} |
|||
@ -1,5 +1,5 @@ |
|||
export * from './autofocus.directive'; |
|||
export * from './ellipsis.directive'; |
|||
export * from './form-keyup-listener.directive'; |
|||
export * from './form-submit.directive'; |
|||
export * from './permission.directive'; |
|||
export * from './visibility.directive'; |
|||
|
|||
@ -0,0 +1 @@ |
|||
<h1> @abp/ng.feature-management </h1> |
|||
@ -0,0 +1,32 @@ |
|||
// Karma configuration file, see link for more information
|
|||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
|||
|
|||
module.exports = function (config) { |
|||
config.set({ |
|||
basePath: '', |
|||
frameworks: ['jasmine', '@angular-devkit/build-angular'], |
|||
plugins: [ |
|||
require('karma-jasmine'), |
|||
require('karma-chrome-launcher'), |
|||
require('karma-jasmine-html-reporter'), |
|||
require('karma-coverage-istanbul-reporter'), |
|||
require('@angular-devkit/build-angular/plugins/karma') |
|||
], |
|||
client: { |
|||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
|||
}, |
|||
coverageIstanbulReporter: { |
|||
dir: require('path').join(__dirname, '../../coverage/feature-management'), |
|||
reports: ['html', 'lcovonly', 'text-summary'], |
|||
fixWebpackSourcePaths: true |
|||
}, |
|||
reporters: ['progress', 'kjhtml'], |
|||
port: 9876, |
|||
colors: true, |
|||
logLevel: config.LOG_INFO, |
|||
autoWatch: true, |
|||
browsers: ['Chrome'], |
|||
singleRun: false, |
|||
restartOnFileChange: true |
|||
}); |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/feature-management", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,4 @@ |
|||
{ |
|||
"name": "@abp/ng.feature-management", |
|||
"version": "0.0.1" |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
import { FeatureManagement } from '../models'; |
|||
|
|||
export class GetFeatures { |
|||
static readonly type = '[FeatureManagement] Get Features'; |
|||
constructor(public payload: FeatureManagement.Provider) {} |
|||
} |
|||
|
|||
export class UpdateFeatures { |
|||
static readonly type = '[FeatureManagement] Update Features'; |
|||
constructor(public payload: FeatureManagement.Provider & FeatureManagement.Features) {} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './feature-management.actions'; |
|||
@ -0,0 +1,32 @@ |
|||
<abp-modal size="md" [(visible)]="visible" [busy]="modalBusy"> |
|||
<ng-template #abpHeader> |
|||
<h3>{{ 'AbpTenantManagement::Permission:ManageFeatures' | abpLocalization }}</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
<form *ngIf="form" (ngSubmit)="save()" [formGroup]="form"> |
|||
<div |
|||
class="row my-3" |
|||
*ngFor="let feature of features$ | async; let i = index" |
|||
[ngSwitch]="feature.valueType.name" |
|||
> |
|||
<div class="col-4">{{ feature.name }}</div> |
|||
<div class="col-8" *ngSwitchCase="'ToggleStringValueType'"> |
|||
<input type="checkbox" name="feature.name" [formControlName]="i" /> |
|||
</div> |
|||
<div class="col-8" *ngSwitchCase="'FreeTextStringValueType'"> |
|||
<input type="text" name="feature.name" [formControlName]="i" /> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button #abpClose type="button" class="btn btn-secondary"> |
|||
{{ 'AbpFeatureManagement::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button iconClass="fa fa-check" (click)="save()"> |
|||
{{ 'AbpFeatureManagement::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,97 @@ |
|||
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; |
|||
import { Select, Store } from '@ngxs/store'; |
|||
import { Observable } from 'rxjs'; |
|||
import { GetFeatures, UpdateFeatures } from '../../actions'; |
|||
import { FeatureManagement } from '../../models/feature-management'; |
|||
import { FeatureManagementState } from '../../states'; |
|||
import { FormGroup, FormControl } from '@angular/forms'; |
|||
import { pluck, tap } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-feature-management', |
|||
templateUrl: './feature-management.component.html', |
|||
}) |
|||
export class FeatureManagementComponent { |
|||
@Input() |
|||
providerKey: string; |
|||
|
|||
@Input() |
|||
providerName: string; |
|||
|
|||
protected _visible; |
|||
|
|||
@Input() |
|||
get visible(): boolean { |
|||
return this._visible; |
|||
} |
|||
|
|||
set visible(value: boolean) { |
|||
this._visible = value; |
|||
this.visibleChange.emit(value); |
|||
|
|||
if (value) this.openModal(); |
|||
} |
|||
|
|||
@Output() |
|||
visibleChange = new EventEmitter<boolean>(); |
|||
|
|||
@Select(FeatureManagementState.getFeatures) |
|||
features$: Observable<FeatureManagement.Feature[]>; |
|||
|
|||
modalBusy: boolean = false; |
|||
|
|||
form: FormGroup; |
|||
|
|||
constructor(private store: Store) {} |
|||
|
|||
openModal() { |
|||
if (!this.providerKey || !this.providerName) { |
|||
throw new Error('Provider Key and Provider Name are required.'); |
|||
} |
|||
|
|||
this.getFeatures(); |
|||
} |
|||
|
|||
getFeatures() { |
|||
this.store |
|||
.dispatch(new GetFeatures({ providerKey: this.providerKey, providerName: this.providerName })) |
|||
.pipe(pluck('FeatureManagementState', 'features')) |
|||
.subscribe(features => { |
|||
this.buildForm(features); |
|||
}); |
|||
} |
|||
|
|||
buildForm(features) { |
|||
const formGroupObj = {}; |
|||
|
|||
for (let i = 0; i < features.length; i++) { |
|||
formGroupObj[i] = new FormControl(features[i].value === 'false' ? null : features[i].value); |
|||
} |
|||
|
|||
this.form = new FormGroup(formGroupObj); |
|||
} |
|||
|
|||
save() { |
|||
this.modalBusy = true; |
|||
|
|||
let features = this.store.selectSnapshot(FeatureManagementState.getFeatures); |
|||
|
|||
features = features.map((feature, i) => ({ |
|||
name: feature.name, |
|||
value: !this.form.value[i] || this.form.value[i] === 'false' ? null : this.form.value[i], |
|||
})); |
|||
|
|||
this.store |
|||
.dispatch( |
|||
new UpdateFeatures({ |
|||
providerKey: this.providerKey, |
|||
providerName: this.providerName, |
|||
features, |
|||
}), |
|||
) |
|||
.subscribe(() => { |
|||
this.modalBusy = false; |
|||
this.visible = false; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './feature-management/feature-management.component'; |
|||
@ -0,0 +1,13 @@ |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { FeatureManagementComponent } from './components/feature-management/feature-management.component'; |
|||
import { NgxsModule } from '@ngxs/store'; |
|||
import { FeatureManagementState } from './states/feature-management.state'; |
|||
|
|||
@NgModule({ |
|||
declarations: [FeatureManagementComponent], |
|||
imports: [CoreModule, ThemeSharedModule, NgxsModule.forFeature([FeatureManagementState])], |
|||
exports: [FeatureManagementComponent], |
|||
}) |
|||
export class FeatureManagementModule {} |
|||
@ -0,0 +1,29 @@ |
|||
export namespace FeatureManagement { |
|||
export interface State { |
|||
features: Feature[]; |
|||
} |
|||
|
|||
export interface ValueType { |
|||
name: string; |
|||
properties: object; |
|||
validator: object; |
|||
} |
|||
|
|||
export interface Feature { |
|||
name: string; |
|||
value: string; |
|||
description?: string; |
|||
valueType?: ValueType; |
|||
depth?: number; |
|||
parentName?: string; |
|||
} |
|||
|
|||
export interface Features { |
|||
features: Feature[]; |
|||
} |
|||
|
|||
export interface Provider { |
|||
providerName: string; |
|||
providerKey: string; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './feature-management'; |
|||
@ -0,0 +1,35 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { RestService, Rest } from '@abp/ng.core'; |
|||
import { Store } from '@ngxs/store'; |
|||
import { Observable } from 'rxjs'; |
|||
import { FeatureManagement } from '../models'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class FeatureManagementService { |
|||
constructor(private rest: RestService, private store: Store) {} |
|||
|
|||
getFeatures(params: FeatureManagement.Provider): Observable<FeatureManagement.Features> { |
|||
const request: Rest.Request<null> = { |
|||
method: 'GET', |
|||
url: '/api/abp/features', |
|||
params, |
|||
}; |
|||
return this.rest.request<FeatureManagement.Provider, FeatureManagement.Features>(request); |
|||
} |
|||
|
|||
updateFeatures({ |
|||
features, |
|||
providerKey, |
|||
providerName, |
|||
}: FeatureManagement.Provider & FeatureManagement.Features): Observable<null> { |
|||
const request: Rest.Request<FeatureManagement.Features> = { |
|||
method: 'PUT', |
|||
url: '/api/abp/features', |
|||
body: { features }, |
|||
params: { providerKey, providerName }, |
|||
}; |
|||
return this.rest.request<FeatureManagement.Features, null>(request); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './feature-management.service'; |
|||
@ -0,0 +1,34 @@ |
|||
import { Action, Selector, State, StateContext } from '@ngxs/store'; |
|||
import { tap } from 'rxjs/operators'; |
|||
import { GetFeatures, UpdateFeatures } from '../actions/feature-management.actions'; |
|||
import { FeatureManagement } from '../models/feature-management'; |
|||
import { FeatureManagementService } from '../services/feature-management.service'; |
|||
|
|||
@State<FeatureManagement.State>({ |
|||
name: 'FeatureManagementState', |
|||
defaults: { features: {} } as FeatureManagement.State, |
|||
}) |
|||
export class FeatureManagementState { |
|||
@Selector() |
|||
static getFeatures({ features }: FeatureManagement.State) { |
|||
return features; |
|||
} |
|||
|
|||
constructor(private featureManagementService: FeatureManagementService) {} |
|||
|
|||
@Action(GetFeatures) |
|||
getFeatures({ patchState }: StateContext<FeatureManagement.State>, { payload }: GetFeatures) { |
|||
return this.featureManagementService.getFeatures(payload).pipe( |
|||
tap(({ features }) => |
|||
patchState({ |
|||
features, |
|||
}), |
|||
), |
|||
); |
|||
} |
|||
|
|||
@Action(UpdateFeatures) |
|||
updateFeatures(_, { payload }: UpdateFeatures) { |
|||
return this.featureManagementService.updateFeatures(payload); |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from "./feature-management.state"; |
|||
@ -0,0 +1,2 @@ |
|||
export * from './lib/feature-management.module'; |
|||
export * from './lib/components'; |
|||
@ -0,0 +1,21 @@ |
|||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
|||
|
|||
import 'zone.js/dist/zone'; |
|||
import 'zone.js/dist/zone-testing'; |
|||
import { getTestBed } from '@angular/core/testing'; |
|||
import { |
|||
BrowserDynamicTestingModule, |
|||
platformBrowserDynamicTesting |
|||
} from '@angular/platform-browser-dynamic/testing'; |
|||
|
|||
declare const require: any; |
|||
|
|||
// First, initialize the Angular testing environment.
|
|||
getTestBed().initTestEnvironment( |
|||
BrowserDynamicTestingModule, |
|||
platformBrowserDynamicTesting() |
|||
); |
|||
// Then we find all the tests.
|
|||
const context = require.context('./', true, /\.spec\.ts$/); |
|||
// And load the modules.
|
|||
context.keys().map(context); |
|||
@ -0,0 +1,26 @@ |
|||
{ |
|||
"extends": "../../tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../out-tsc/lib", |
|||
"target": "es2015", |
|||
"declaration": true, |
|||
"inlineSources": true, |
|||
"types": [], |
|||
"lib": [ |
|||
"dom", |
|||
"es2018" |
|||
] |
|||
}, |
|||
"angularCompilerOptions": { |
|||
"annotateForClosureCompiler": true, |
|||
"skipTemplateCodegen": true, |
|||
"strictMetadataEmit": true, |
|||
"fullTemplateTypeCheck": true, |
|||
"strictInjectionParameters": true, |
|||
"enableResourceInlining": true |
|||
}, |
|||
"exclude": [ |
|||
"src/test.ts", |
|||
"**/*.spec.ts" |
|||
] |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
{ |
|||
"extends": "../../tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../out-tsc/spec", |
|||
"types": [ |
|||
"jasmine", |
|||
"node" |
|||
] |
|||
}, |
|||
"files": [ |
|||
"src/test.ts" |
|||
], |
|||
"include": [ |
|||
"**/*.spec.ts", |
|||
"**/*.d.ts" |
|||
] |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
{ |
|||
"extends": "../../tslint.json", |
|||
"rules": { |
|||
"directive-selector": [ |
|||
true, |
|||
"attribute", |
|||
"abp", |
|||
"camelCase" |
|||
], |
|||
"component-selector": [ |
|||
true, |
|||
"element", |
|||
"abp", |
|||
"kebab-case" |
|||
] |
|||
} |
|||
} |
|||
@ -1,26 +1 @@ |
|||
<abp-layout> |
|||
<ul class="navbar-nav mr-auto"> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="/"> |
|||
{{ '::Menu:Home' | abpLocalization }} |
|||
</a> |
|||
</li> |
|||
</ul> |
|||
|
|||
<span id="main-navbar-tools"> |
|||
<span> |
|||
<div class="dropdown d-inline" ngbDropdown> |
|||
<a class="btn btn-link dropdown-toggle" role="button" data-toggle="dropdown" ngbDropdownToggle> |
|||
English |
|||
</a> |
|||
|
|||
<div class="dropdown-menu" ngbDropdownMenu> |
|||
<a class="dropdown-item">Čeština</a> |
|||
<a class="dropdown-item">Português</a> |
|||
<a class="dropdown-item">Türkçe</a> |
|||
<a class="dropdown-item">简体中文</a> |
|||
</div> |
|||
</div> |
|||
</span> |
|||
</span> |
|||
</abp-layout> |
|||
<router-outlet></router-outlet> |
|||
|
|||
Loading…
Reference in new issue