mirror of https://github.com/abpframework/abp.git
32 changed files with 13249 additions and 7 deletions
@ -0,0 +1,36 @@ |
|||
{ |
|||
"extends": ["../../.eslintrc.json"], |
|||
"ignorePatterns": ["!**/*"], |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.ts"], |
|||
"extends": [ |
|||
"plugin:@nrwl/nx/angular", |
|||
"plugin:@angular-eslint/template/process-inline-templates" |
|||
], |
|||
"rules": { |
|||
"@angular-eslint/directive-selector": [ |
|||
"error", |
|||
{ |
|||
"type": "attribute", |
|||
"prefix": "abp", |
|||
"style": "camelCase" |
|||
} |
|||
], |
|||
"@angular-eslint/component-selector": [ |
|||
"error", |
|||
{ |
|||
"type": "element", |
|||
"prefix": "abp", |
|||
"style": "kebab-case" |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"files": ["*.html"], |
|||
"extends": ["plugin:@nrwl/nx/angular-template"], |
|||
"rules": {} |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<h1> @abp/ng.feature-management </h1> |
|||
|
|||
[docs.abp.io](https://docs.abp.io) |
|||
@ -0,0 +1,20 @@ |
|||
module.exports = { |
|||
displayName: 'feature-management', |
|||
preset: '../../jest.preset.js', |
|||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], |
|||
globals: { |
|||
'ts-jest': { |
|||
tsconfig: '<rootDir>/tsconfig.spec.json', |
|||
stringifyContentPathRegex: '\\.(html|svg)$', |
|||
}, |
|||
}, |
|||
coverageDirectory: '../../coverage/packages/feature-management', |
|||
transform: { |
|||
'^.+\\.(ts|js|html)$': 'jest-preset-angular', |
|||
}, |
|||
snapshotSerializers: [ |
|||
'jest-preset-angular/build/serializers/no-ng-attributes', |
|||
'jest-preset-angular/build/serializers/ng-snapshot', |
|||
'jest-preset-angular/build/serializers/html-comment', |
|||
], |
|||
}; |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/packages/feature-management", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
}, |
|||
"allowedNonPeerDependencies": ["@abp/ng.theme.shared"] |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
{ |
|||
"name": "@abp/ng.feature-management", |
|||
"version": "4.4.0", |
|||
"homepage": "https://abp.io", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://github.com/abpframework/abp.git" |
|||
}, |
|||
"dependencies": { |
|||
"@abp/ng.theme.shared": "~4.4.0", |
|||
"tslib": "^2.0.0" |
|||
}, |
|||
"publishConfig": { |
|||
"access": "public" |
|||
} |
|||
} |
|||
@ -0,0 +1,126 @@ |
|||
<abp-modal *ngIf="visible" size="lg" [(visible)]="visible" [busy]="modalBusy"> |
|||
<ng-template #abpHeader> |
|||
<h3>{{ 'AbpFeatureManagement::Features' | abpLocalization }}</h3> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpBody> |
|||
<div class="row"> |
|||
<div class="col-md-4"> |
|||
<ul |
|||
ngbNav |
|||
#nav="ngbNav" |
|||
[(activeId)]="selectedGroupDisplayName" |
|||
class="nav-pills" |
|||
orientation="vertical" |
|||
> |
|||
<li |
|||
*ngFor="let group of groups; trackBy: track.by('name')" |
|||
[ngbNavItem]="group.displayName" |
|||
> |
|||
<a ngbNavLink>{{ group.displayName }}</a> |
|||
<ng-template ngbNavContent> |
|||
<h4>{{ selectedGroupDisplayName }}</h4> |
|||
<hr class="mt-2 mb-3" /> |
|||
|
|||
<div |
|||
class="mt-2" |
|||
*ngFor="let feature of features[group.name]; let i = index; trackBy: track.by('id')" |
|||
[ngStyle]="feature.style" |
|||
[ngSwitch]="feature.valueType?.name" |
|||
(keyup.enter)="save()" |
|||
> |
|||
<ng-container *ngSwitchCase="valueTypes.ToggleStringValueType"> |
|||
<div class="custom-checkbox custom-control"> |
|||
<input |
|||
class="custom-control-input" |
|||
type="checkbox" |
|||
[id]="feature.name" |
|||
[(ngModel)]="feature.value" |
|||
(ngModelChange)="onCheckboxClick($event, feature)" |
|||
/> |
|||
|
|||
<label class="custom-control-label" [htmlFor]="feature.name">{{ |
|||
feature.displayName |
|||
}}</label> |
|||
<ng-container |
|||
*ngTemplateOutlet="descTmp; context: { $implicit: feature.description }" |
|||
></ng-container> |
|||
</div> |
|||
</ng-container> |
|||
<ng-container *ngSwitchCase="valueTypes.FreeTextStringValueType"> |
|||
<div class="form-group"> |
|||
<label [htmlFor]="feature.name">{{ feature.displayName }}</label> |
|||
<input |
|||
class="form-control" |
|||
type="text" |
|||
[id]="feature.name" |
|||
[(ngModel)]="feature.value" |
|||
[abpFeatureManagementFreeText]="feature" |
|||
/> |
|||
|
|||
<ng-container |
|||
*ngTemplateOutlet="descTmp; context: { $implicit: feature.description }" |
|||
></ng-container> |
|||
</div> |
|||
</ng-container> |
|||
<ng-container *ngSwitchCase="valueTypes.SelectionStringValueType"> |
|||
<ng-container *ngIf="feature.valueType.itemSource?.items?.length"> |
|||
<div class="form-group"> |
|||
<label [htmlFor]="feature.name">{{ feature.displayName }}</label> |
|||
<select |
|||
class="form-control custom-select" |
|||
[id]="feature.name" |
|||
[(ngModel)]="feature.value" |
|||
> |
|||
<option |
|||
*ngFor=" |
|||
let item of feature.valueType.itemSource?.items; |
|||
trackBy: track.by('value') |
|||
" |
|||
[ngValue]="item.value" |
|||
> |
|||
{{ |
|||
item.displayText?.resourceName + '::' + item.displayText?.name |
|||
| abpLocalization |
|||
}} |
|||
</option> |
|||
</select> |
|||
<ng-container |
|||
*ngTemplateOutlet="descTmp; context: { $implicit: feature.description }" |
|||
></ng-container> |
|||
</div> |
|||
</ng-container> |
|||
</ng-container> |
|||
<ng-container *ngSwitchDefault>{{ feature.displayName }}</ng-container> |
|||
</div> |
|||
</ng-template> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
|
|||
<ng-template #descTmp let-description> |
|||
<small *ngIf="description" class="form-text text-muted">{{ description }}</small> |
|||
</ng-template> |
|||
|
|||
<div class="col-md-8"><div [ngbNavOutlet]="nav"></div></div> |
|||
|
|||
<div class="mx-3" *ngIf="!groups.length"> |
|||
{{ 'AbpFeatureManagement::NoFeatureFoundMessage' | abpLocalization }} |
|||
</div> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
<ng-template #abpFooter> |
|||
<button abpClose type="button" class="btn btn-secondary"> |
|||
{{ 'AbpFeatureManagement::Cancel' | abpLocalization }} |
|||
</button> |
|||
<abp-button |
|||
*ngIf="groups.length" |
|||
iconClass="fa fa-check" |
|||
[disabled]="modalBusy" |
|||
(click)="save()" |
|||
> |
|||
{{ 'AbpFeatureManagement::Save' | abpLocalization }} |
|||
</abp-button> |
|||
</ng-template> |
|||
</abp-modal> |
|||
@ -0,0 +1,214 @@ |
|||
import { |
|||
AbpApplicationConfigurationService, |
|||
ConfigStateService, |
|||
TrackByService, |
|||
} from '@abp/ng.core'; |
|||
import { LocaleDirection } from '@abp/ng.theme.shared'; |
|||
import { Component, EventEmitter, Input, Output } from '@angular/core'; |
|||
import { Store } from '@ngxs/store'; |
|||
import { finalize, tap } from 'rxjs/operators'; |
|||
import { FeatureManagement } from '../../models/feature-management'; |
|||
import { FeaturesService } from '../../proxy/feature-management/features.service'; |
|||
import { |
|||
FeatureDto, |
|||
FeatureGroupDto, |
|||
UpdateFeatureDto, |
|||
} from '../../proxy/feature-management/models'; |
|||
|
|||
enum ValueTypes { |
|||
ToggleStringValueType = 'ToggleStringValueType', |
|||
FreeTextStringValueType = 'FreeTextStringValueType', |
|||
SelectionStringValueType = 'SelectionStringValueType', |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'abp-feature-management', |
|||
templateUrl: './feature-management.component.html', |
|||
exportAs: 'abpFeatureManagement', |
|||
}) |
|||
export class FeatureManagementComponent |
|||
implements |
|||
FeatureManagement.FeatureManagementComponentInputs, |
|||
FeatureManagement.FeatureManagementComponentOutputs { |
|||
@Input() |
|||
providerKey: string; |
|||
|
|||
@Input() |
|||
providerName: string; |
|||
|
|||
selectedGroupDisplayName: string; |
|||
|
|||
groups: Pick<FeatureGroupDto, 'name' | 'displayName'>[] = []; |
|||
|
|||
features: { |
|||
[group: string]: Array<FeatureDto & { style?: { [key: string]: number }; initialValue: any }>; |
|||
}; |
|||
|
|||
valueTypes = ValueTypes; |
|||
|
|||
protected _visible; |
|||
|
|||
@Input() |
|||
get visible(): boolean { |
|||
return this._visible; |
|||
} |
|||
|
|||
set visible(value: boolean) { |
|||
if (this._visible === value) return; |
|||
|
|||
this._visible = value; |
|||
this.visibleChange.emit(value); |
|||
if (value) this.openModal(); |
|||
} |
|||
|
|||
@Output() readonly visibleChange = new EventEmitter<boolean>(); |
|||
|
|||
modalBusy = false; |
|||
|
|||
constructor( |
|||
public readonly track: TrackByService, |
|||
protected service: FeaturesService, |
|||
protected store: Store, |
|||
protected configState: ConfigStateService, |
|||
protected appConfigService: AbpApplicationConfigurationService, |
|||
) {} |
|||
|
|||
openModal() { |
|||
if (!this.providerName) { |
|||
throw new Error('providerName is required.'); |
|||
} |
|||
|
|||
this.getFeatures(); |
|||
} |
|||
|
|||
getFeatures() { |
|||
this.service.get(this.providerName, this.providerKey).subscribe(res => { |
|||
if (!res.groups?.length) return; |
|||
this.groups = res.groups.map(({ name, displayName }) => ({ name, displayName })); |
|||
this.selectedGroupDisplayName = this.groups[0].displayName; |
|||
this.features = res.groups.reduce( |
|||
(acc, val) => ({ |
|||
...acc, |
|||
[val.name]: mapFeatures(val.features, document.body.dir as LocaleDirection), |
|||
}), |
|||
{}, |
|||
); |
|||
}); |
|||
} |
|||
|
|||
save() { |
|||
if (this.modalBusy) return; |
|||
|
|||
const changedFeatures = [] as UpdateFeatureDto[]; |
|||
|
|||
Object.keys(this.features).forEach(key => { |
|||
this.features[key].forEach(feature => { |
|||
if (feature.value !== feature.initialValue) |
|||
changedFeatures.push({ name: feature.name, value: `${feature.value}` }); |
|||
}); |
|||
}); |
|||
|
|||
if (!changedFeatures.length) { |
|||
this.visible = false; |
|||
return; |
|||
} |
|||
|
|||
this.modalBusy = true; |
|||
this.service |
|||
.update(this.providerName, this.providerKey, { features: changedFeatures }) |
|||
.pipe(finalize(() => (this.modalBusy = false))) |
|||
.subscribe(() => { |
|||
this.visible = false; |
|||
|
|||
if (!this.providerKey) { |
|||
// to refresh host's features
|
|||
this.appConfigService |
|||
.get() |
|||
.pipe(tap(res => this.configState.setState(res))) |
|||
.subscribe(); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
onCheckboxClick(val: boolean, feature: FeatureDto) { |
|||
if (val) { |
|||
this.checkToggleAncestors(feature); |
|||
} else { |
|||
this.uncheckToggleDescendants(feature); |
|||
} |
|||
} |
|||
|
|||
private uncheckToggleDescendants(feature: FeatureDto) { |
|||
this.findAllDescendantsOfByType(feature, ValueTypes.ToggleStringValueType).forEach(node => |
|||
this.setFeatureValue(node, false), |
|||
); |
|||
} |
|||
|
|||
private checkToggleAncestors(feature: FeatureDto) { |
|||
this.findAllAncestorsOfByType(feature, ValueTypes.ToggleStringValueType).forEach(node => |
|||
this.setFeatureValue(node, true), |
|||
); |
|||
} |
|||
|
|||
private findAllAncestorsOfByType(feature: FeatureDto, type: ValueTypes) { |
|||
let parent = this.findParentByType(feature, type); |
|||
const ancestors = []; |
|||
while (parent) { |
|||
ancestors.push(parent); |
|||
parent = this.findParentByType(parent, type); |
|||
} |
|||
return ancestors; |
|||
} |
|||
|
|||
private findAllDescendantsOfByType(feature: FeatureDto, type: ValueTypes) { |
|||
const descendants = []; |
|||
const queue = [feature]; |
|||
|
|||
while (queue.length) { |
|||
const node = queue.pop(); |
|||
const newDescendants = this.findChildrenByType(node, type); |
|||
descendants.push(...newDescendants); |
|||
queue.push(...newDescendants); |
|||
} |
|||
|
|||
return descendants; |
|||
} |
|||
|
|||
private findParentByType(feature: FeatureDto, type: ValueTypes) { |
|||
return this.getCurrentGroup().find( |
|||
f => f.valueType.name === type && f.name === feature.parentName, |
|||
); |
|||
} |
|||
|
|||
private findChildrenByType(feature: FeatureDto, type: ValueTypes) { |
|||
return this.getCurrentGroup().filter( |
|||
f => f.valueType.name === type && f.parentName === feature.name, |
|||
); |
|||
} |
|||
|
|||
private getCurrentGroup() { |
|||
return this.features[this.selectedGroupDisplayName] ?? []; |
|||
} |
|||
|
|||
private setFeatureValue(feature: FeatureDto, val: boolean) { |
|||
feature.value = val as any; |
|||
} |
|||
} |
|||
|
|||
function mapFeatures(features: FeatureDto[], dir: LocaleDirection) { |
|||
const margin = `margin-${dir === 'rtl' ? 'right' : 'left'}.px`; |
|||
|
|||
return features.map(feature => { |
|||
const value = |
|||
feature.valueType?.name === ValueTypes.ToggleStringValueType |
|||
? (feature.value || '').toLowerCase() === 'true' |
|||
: feature.value; |
|||
|
|||
return { |
|||
...feature, |
|||
value, |
|||
initialValue: value, |
|||
style: { [margin]: feature.depth * 20 }, |
|||
}; |
|||
}); |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './feature-management/feature-management.component'; |
|||
@ -0,0 +1,38 @@ |
|||
import { Directive, Input, HostBinding } from '@angular/core'; |
|||
|
|||
// TODO: improve this type
|
|||
export interface FreeTextType { |
|||
valueType: { |
|||
validator: { |
|||
name: string; |
|||
}; |
|||
}; |
|||
} |
|||
|
|||
export const INPUT_TYPES = { |
|||
numeric: 'number', |
|||
default: 'text', |
|||
}; |
|||
|
|||
@Directive({ |
|||
selector: 'input[abpFeatureManagementFreeText]', |
|||
exportAs: 'inputAbpFeatureManagementFreeText', |
|||
}) |
|||
export class FreeTextInputDirective { |
|||
_feature: FreeTextType; |
|||
@Input('abpFeatureManagementFreeText') set feature(val: FreeTextType) { |
|||
this._feature = val; |
|||
this.setInputType(); |
|||
} |
|||
|
|||
get feature() { |
|||
return this._feature; |
|||
} |
|||
|
|||
@HostBinding('type') type: string; |
|||
|
|||
private setInputType() { |
|||
const validatorType = this.feature?.valueType?.validator?.name.toLowerCase(); |
|||
this.type = INPUT_TYPES[validatorType] ?? INPUT_TYPES.default; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './free-text-input.directive'; |
|||
@ -0,0 +1,3 @@ |
|||
export const enum eFeatureManagementComponents { |
|||
FeatureManagement = 'FeatureManagement.FeatureManagementComponent', |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { NgxsModule } from '@ngxs/store'; |
|||
import { FeatureManagementComponent } from './components/feature-management/feature-management.component'; |
|||
import { FreeTextInputDirective } from './directives/free-text-input.directive'; |
|||
|
|||
const exported = [FeatureManagementComponent, FreeTextInputDirective]; |
|||
|
|||
@NgModule({ |
|||
declarations: [...exported], |
|||
imports: [CoreModule, ThemeSharedModule, NgbNavModule, NgxsModule.forFeature([])], |
|||
exports: [...exported], |
|||
}) |
|||
export class FeatureManagementModule {} |
|||
@ -0,0 +1,13 @@ |
|||
import { EventEmitter } from '@angular/core'; |
|||
|
|||
export namespace FeatureManagement { |
|||
export interface FeatureManagementComponentInputs { |
|||
visible: boolean; |
|||
readonly providerName: string; |
|||
readonly providerKey: string; |
|||
} |
|||
|
|||
export interface FeatureManagementComponentOutputs { |
|||
readonly visibleChange: EventEmitter<boolean>; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export * from './feature-management'; |
|||
@ -0,0 +1,13 @@ |
|||
# Proxy Generation Output |
|||
|
|||
This directory includes the output of the latest proxy generation. |
|||
The files and folders in it will be overwritten when proxy generation is run again. |
|||
Therefore, please do not place your own content in this folder. |
|||
|
|||
In addition, `generate-proxy.json` works like a lock file. |
|||
It includes information used by the proxy generator, so please do not delete or modify it. |
|||
|
|||
Finally, the name of the files and folders should not be changed for two reasons: |
|||
- Proxy generator will keep creating them at those paths and you will have multiple copies of the same content. |
|||
- ABP Suite generates files which include imports from this folder. |
|||
|
|||
@ -0,0 +1,29 @@ |
|||
import type { GetFeatureListResultDto, UpdateFeaturesDto } from './models'; |
|||
import { RestService } from '@abp/ng.core'; |
|||
import { Injectable } from '@angular/core'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class FeaturesService { |
|||
apiName = 'AbpFeatureManagement'; |
|||
|
|||
get = (providerName: string, providerKey: string) => |
|||
this.restService.request<any, GetFeatureListResultDto>({ |
|||
method: 'GET', |
|||
url: '/api/feature-management/features', |
|||
params: { providerName, providerKey }, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
update = (providerName: string, providerKey: string, input: UpdateFeaturesDto) => |
|||
this.restService.request<any, void>({ |
|||
method: 'PUT', |
|||
url: '/api/feature-management/features', |
|||
params: { providerName, providerKey }, |
|||
body: input, |
|||
}, |
|||
{ apiName: this.apiName }); |
|||
|
|||
constructor(private restService: RestService) {} |
|||
} |
|||
@ -0,0 +1,2 @@ |
|||
export * from './features.service'; |
|||
export * from './models'; |
|||
@ -0,0 +1,36 @@ |
|||
import type { IStringValueType } from '../validation/string-values/models'; |
|||
|
|||
export interface FeatureDto { |
|||
name: string; |
|||
displayName: string; |
|||
value: string; |
|||
provider: FeatureProviderDto; |
|||
description: string; |
|||
valueType: IStringValueType; |
|||
depth: number; |
|||
parentName: string; |
|||
} |
|||
|
|||
export interface FeatureGroupDto { |
|||
name: string; |
|||
displayName: string; |
|||
features: FeatureDto[]; |
|||
} |
|||
|
|||
export interface FeatureProviderDto { |
|||
name: string; |
|||
key: string; |
|||
} |
|||
|
|||
export interface GetFeatureListResultDto { |
|||
groups: FeatureGroupDto[]; |
|||
} |
|||
|
|||
export interface UpdateFeatureDto { |
|||
name: string; |
|||
value: string; |
|||
} |
|||
|
|||
export interface UpdateFeaturesDto { |
|||
features: UpdateFeatureDto[]; |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,2 @@ |
|||
export * as FeatureManagement from './feature-management'; |
|||
export * as Validation from './validation'; |
|||
@ -0,0 +1 @@ |
|||
export * as StringValues from './string-values'; |
|||
@ -0,0 +1 @@ |
|||
export * from './models'; |
|||
@ -0,0 +1,13 @@ |
|||
|
|||
export interface IStringValueType { |
|||
name: string; |
|||
item: object; |
|||
properties: Record<string, object>; |
|||
validator: IValueValidator; |
|||
} |
|||
|
|||
export interface IValueValidator { |
|||
name: string; |
|||
item: object; |
|||
properties: Record<string, object>; |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
export * from './lib/feature-management.module'; |
|||
export * from './lib/components'; |
|||
export * from './lib/enums/components'; |
|||
export * from './lib/proxy/feature-management'; |
|||
export * from './lib/proxy/validation/string-values'; |
|||
@ -0,0 +1 @@ |
|||
import 'jest-preset-angular/setup-jest'; |
|||
@ -0,0 +1,13 @@ |
|||
{ |
|||
"extends": "../../tsconfig.base.json", |
|||
"files": [], |
|||
"include": [], |
|||
"references": [ |
|||
{ |
|||
"path": "./tsconfig.lib.json" |
|||
}, |
|||
{ |
|||
"path": "./tsconfig.spec.json" |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
{ |
|||
"extends": "./tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../dist/out-tsc", |
|||
"target": "es2015", |
|||
"declaration": true, |
|||
"declarationMap": true, |
|||
"inlineSources": true, |
|||
"types": [], |
|||
"lib": ["dom", "es2018"] |
|||
}, |
|||
"exclude": ["src/test-setup.ts", "**/*.spec.ts"], |
|||
"include": ["**/*.ts"] |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ |
|||
{ |
|||
"extends": "./tsconfig.lib.json", |
|||
"compilerOptions": { |
|||
"declarationMap": false |
|||
}, |
|||
"angularCompilerOptions": { |
|||
"compilationMode": "partial", |
|||
"enableIvy": false |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"extends": "./tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../../dist/out-tsc", |
|||
"module": "commonjs", |
|||
"types": ["jest", "node"] |
|||
}, |
|||
"files": ["src/test-setup.ts"], |
|||
"include": ["**/*.spec.ts", "**/*.d.ts"] |
|||
} |
|||
Loading…
Reference in new issue