mirror of https://github.com/abpframework/abp.git
21 changed files with 393 additions and 0 deletions
@ -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,8 @@ |
|||
{ |
|||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/feature-management", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
}, |
|||
"deleteDestPath": false |
|||
} |
|||
@ -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> |
|||
<button type="button" class="btn btn-primary" (click)="save()"> |
|||
{{ 'AbpFeatureManagement::Save' | abpLocalization }} |
|||
</button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,106 @@ |
|||
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 implements OnChanges { |
|||
@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); |
|||
} |
|||
|
|||
@Output() |
|||
visibleChange = new EventEmitter<boolean>(); |
|||
|
|||
@Select(FeatureManagementState.getFeatures) |
|||
features$: Observable<FeatureManagement.Feature[]>; |
|||
|
|||
modalBusy: boolean = false; |
|||
|
|||
form: FormGroup; |
|||
|
|||
constructor(private store: Store) {} |
|||
|
|||
ngOnChanges({ visible }: SimpleChanges): void { |
|||
if (!visible) return; |
|||
|
|||
if (visible.currentValue) { |
|||
this.openModal(); |
|||
} else if (this.visible && visible.currentValue === false) { |
|||
this.visible = false; |
|||
} |
|||
} |
|||
|
|||
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); |
|||
this.visible = true; |
|||
}); |
|||
} |
|||
|
|||
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" |
|||
] |
|||
} |
|||
} |
|||
Loading…
Reference in new issue