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