mirror of https://github.com/abpframework/abp.git
58 changed files with 3733 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||
{ |
|||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/theme-shared/extensions", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
}, |
|||
"whitelistedNonPeerDependencies": ["@abp/ng.theme.shared"] |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@Injectable() |
|||
export class DateTimeAdapter { |
|||
value: NgbDateTimeStruct; |
|||
|
|||
fromModel(value: string | Date): NgbDateTimeStruct | null { |
|||
if (!value) return null; |
|||
|
|||
const date = new Date(value); |
|||
|
|||
if (isNaN((date as unknown) as number)) return null; |
|||
|
|||
this.value = { |
|||
year: date.getFullYear(), |
|||
month: date.getMonth() + 1, |
|||
day: date.getDate(), |
|||
hour: date.getHours(), |
|||
minute: date.getMinutes(), |
|||
second: date.getSeconds(), |
|||
}; |
|||
|
|||
return this.value; |
|||
} |
|||
|
|||
toModel(value: NgbDateTimeStruct | null): string { |
|||
if (!value) return ''; |
|||
|
|||
const now = new Date(); |
|||
|
|||
value = { |
|||
year: now.getUTCFullYear(), |
|||
month: now.getMonth() + 1, |
|||
day: now.getDate(), |
|||
hour: 0, |
|||
minute: 0, |
|||
second: 0, |
|||
...this.value, |
|||
...value, |
|||
}; |
|||
|
|||
const date = new Date( |
|||
value.year, |
|||
value.month - 1, |
|||
value.day, |
|||
value.hour, |
|||
value.minute, |
|||
value.second, |
|||
); |
|||
|
|||
return new Date(date).toISOString(); |
|||
} |
|||
} |
|||
|
|||
type NgbDateTimeStruct = NgbDateStruct & NgbTimeStruct; |
|||
@ -0,0 +1,29 @@ |
|||
import { formatDate } from '@angular/common'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@Injectable() |
|||
export class DateAdapter extends NgbDateAdapter<string> { |
|||
fromModel(value: string | Date): NgbDateStruct | null { |
|||
if (!value) return null; |
|||
|
|||
const date = new Date(value); |
|||
|
|||
if (isNaN((date as unknown) as number)) return null; |
|||
|
|||
return { |
|||
day: date.getDate(), |
|||
month: date.getMonth() + 1, |
|||
year: date.getFullYear(), |
|||
}; |
|||
} |
|||
|
|||
toModel(value: NgbDateStruct | null): string { |
|||
if (!value) return ''; |
|||
|
|||
const date = new Date(value.year, value.month - 1, value.day); |
|||
const formattedDate = formatDate(date, 'yyyy-MM-dd', 'en'); |
|||
|
|||
return formattedDate; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
import { formatDate } from '@angular/common'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { NgbTimeAdapter, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@Injectable() |
|||
export class TimeAdapter extends NgbTimeAdapter<string> { |
|||
fromModel(value: string | Date): NgbTimeStruct | null { |
|||
if (!value) return null; |
|||
|
|||
const date = isTimeStr(value) |
|||
? new Date(0, 0, 1, ...value.split(':').map(Number)) |
|||
: new Date(value); |
|||
|
|||
if (isNaN((date as unknown) as number)) return null; |
|||
|
|||
return { |
|||
hour: date.getHours(), |
|||
minute: date.getMinutes(), |
|||
second: date.getSeconds(), |
|||
}; |
|||
} |
|||
|
|||
toModel(value: NgbTimeStruct | null): string { |
|||
if (!value) return ''; |
|||
|
|||
const date = new Date(0, 0, 1, value.hour, value.minute, value.second); |
|||
const formattedDate = formatDate(date, 'HH:mm', 'en'); |
|||
|
|||
return formattedDate; |
|||
} |
|||
} |
|||
|
|||
function isTimeStr(value: string | Date): value is string { |
|||
return /^((2[123])|[01][0-9])(\:[0-5][0-9]){1,2}$/.test(String(value)); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
import { Directive, Injector, Input } from '@angular/core'; |
|||
import { ActionData, ActionList } from '../../models/actions'; |
|||
import { ExtensionsService } from '../../services/extensions.service'; |
|||
import { EXTENSIONS_ACTION_TYPE, EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; |
|||
|
|||
// tslint:disable: directive-class-suffix
|
|||
// Fix for https://github.com/angular/angular/issues/23904
|
|||
// @dynamic
|
|||
@Directive() |
|||
export abstract class AbstractActionsComponent<L extends ActionList<any>> extends ActionData< |
|||
InferredRecord<L> |
|||
> { |
|||
readonly actionList: L; |
|||
|
|||
readonly getInjected: InferredData<L>['getInjected']; |
|||
|
|||
@Input() readonly record: InferredData<L>['record']; |
|||
|
|||
constructor(injector: Injector) { |
|||
super(); |
|||
|
|||
// tslint:disable-next-line
|
|||
this.getInjected = injector.get.bind(injector); |
|||
const extensions = injector.get(ExtensionsService); |
|||
const name = injector.get(EXTENSIONS_IDENTIFIER); |
|||
const type = injector.get(EXTENSIONS_ACTION_TYPE); |
|||
this.actionList = (extensions[type].get(name).actions as unknown) as L; |
|||
} |
|||
} |
|||
|
|||
type InferredData<L> = ActionData<InferredRecord<L>>; |
|||
type InferredRecord<L> = L extends ActionList<infer R> ? R : never; |
|||
@ -0,0 +1,74 @@ |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Input, |
|||
Optional, |
|||
SkipSelf, |
|||
ViewChild, |
|||
} from '@angular/core'; |
|||
import { ControlContainer } from '@angular/forms'; |
|||
import { |
|||
NgbDateAdapter, |
|||
NgbInputDatepicker, |
|||
NgbTimeAdapter, |
|||
NgbTimepicker, |
|||
} from '@ng-bootstrap/ng-bootstrap'; |
|||
import { DateTimeAdapter } from '../../adapters/date-time.adapter'; |
|||
import { FormProp } from '../../models/form-props'; |
|||
import { selfFactory } from '../../utils/factory.util'; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpDateTimePicker', |
|||
selector: 'abp-date-time-picker', |
|||
template: ` |
|||
<input |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
(ngModelChange)="setTime($event)" |
|||
(click)="datepicker.open()" |
|||
(keyup.space)="datepicker.open()" |
|||
ngbDatepicker |
|||
#datepicker="ngbDatepicker" |
|||
type="text" |
|||
class="form-control" |
|||
/> |
|||
<ngb-timepicker |
|||
#timepicker |
|||
[formControlName]="prop.name" |
|||
(ngModelChange)="setDate($event)" |
|||
></ngb-timepicker> |
|||
`,
|
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
viewProviders: [ |
|||
{ |
|||
provide: ControlContainer, |
|||
useFactory: selfFactory, |
|||
deps: [[new Optional(), new SkipSelf(), ControlContainer]], |
|||
}, |
|||
{ |
|||
provide: NgbDateAdapter, |
|||
useClass: DateTimeAdapter, |
|||
}, |
|||
{ |
|||
provide: NgbTimeAdapter, |
|||
useClass: DateTimeAdapter, |
|||
}, |
|||
], |
|||
}) |
|||
export class DateTimePickerComponent { |
|||
@Input() prop: FormProp; |
|||
|
|||
@ViewChild(NgbInputDatepicker) date: NgbInputDatepicker; |
|||
@ViewChild(NgbTimepicker) time: NgbTimepicker; |
|||
|
|||
constructor(public readonly cdRef: ChangeDetectorRef) {} |
|||
|
|||
setDate(datestr: string) { |
|||
this.date.writeValue(datestr); |
|||
} |
|||
|
|||
setTime(datestr: string) { |
|||
this.time.writeValue(datestr); |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
<div class="form-group" [abpPermission]="prop.permission" [ngSwitch]="getComponent(prop)"> |
|||
<ng-template ngSwitchCase="input"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<input |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[autocomplete]="prop.autocomplete" |
|||
[type]="getType(prop)" |
|||
[abpDisabled]="prop.disabled(data)" |
|||
[readonly]="prop.readonly(data)" |
|||
class="form-control" |
|||
/> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="checkbox"> |
|||
<div class="custom-checkbox custom-control" validationTarget> |
|||
<input |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="prop.disabled(data)" |
|||
type="checkbox" |
|||
class="custom-control-input" |
|||
/> |
|||
<label [htmlFor]="prop.id" class="custom-control-label" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="select"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<select |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="prop.disabled(data)" |
|||
class="custom-select form-control" |
|||
> |
|||
<option |
|||
*ngFor="let option of prop.options(data) | async; trackBy: track.by('value')" |
|||
[ngValue]="option.value" |
|||
>{{ option.key }}</option |
|||
> |
|||
</select> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="date"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<input |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
(click)="datepicker.open()" |
|||
(keyup.space)="datepicker.open()" |
|||
ngbDatepicker |
|||
#datepicker="ngbDatepicker" |
|||
type="text" |
|||
class="form-control" |
|||
/> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="time"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<ngb-timepicker [formControlName]="prop.name"></ngb-timepicker> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="dateTime"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<abp-date-time-picker [prop]="prop"></abp-date-time-picker> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="textarea"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<textarea |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="prop.disabled(data)" |
|||
[readonly]="prop.readonly(data)" |
|||
class="form-control" |
|||
></textarea> |
|||
</ng-template> |
|||
</div> |
|||
@ -0,0 +1,80 @@ |
|||
import { TrackByService } from '@abp/ng.core'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Input, |
|||
Optional, |
|||
SkipSelf, |
|||
} from '@angular/core'; |
|||
import { ControlContainer, Validators } from '@angular/forms'; |
|||
import { NgbDateAdapter, NgbTimeAdapter } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { DateAdapter } from '../../adapters/date.adapter'; |
|||
import { TimeAdapter } from '../../adapters/time.adapter'; |
|||
import { ePropType } from '../../enums/props.enum'; |
|||
import { FormProp } from '../../models/form-props'; |
|||
import { PropData } from '../../models/props'; |
|||
import { selfFactory } from '../../utils/factory.util'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-extensible-form-prop', |
|||
templateUrl: './extensible-form-prop.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
viewProviders: [ |
|||
{ |
|||
provide: ControlContainer, |
|||
useFactory: selfFactory, |
|||
deps: [[new Optional(), new SkipSelf(), ControlContainer]], |
|||
}, |
|||
{ provide: NgbDateAdapter, useClass: DateAdapter }, |
|||
{ provide: NgbTimeAdapter, useClass: TimeAdapter }, |
|||
], |
|||
}) |
|||
export class ExtensibleFormPropComponent { |
|||
@Input() data: PropData; |
|||
|
|||
@Input() prop: FormProp; |
|||
|
|||
constructor(public readonly cdRef: ChangeDetectorRef, public readonly track: TrackByService) {} |
|||
|
|||
getAsterisk(prop: FormProp, data: PropData): string { |
|||
return prop.validators(data).some(validator => validator === Validators.required) ? '*' : ''; |
|||
} |
|||
|
|||
getComponent(prop: FormProp): string { |
|||
if (prop.options) return 'select'; |
|||
|
|||
switch (prop.type) { |
|||
case ePropType.Boolean: |
|||
return 'checkbox'; |
|||
case ePropType.Date: |
|||
return 'date'; |
|||
case ePropType.DateTime: |
|||
return 'dateTime'; |
|||
case ePropType.Text: |
|||
return 'textarea'; |
|||
case ePropType.Time: |
|||
return 'time'; |
|||
default: |
|||
return 'input'; |
|||
} |
|||
} |
|||
|
|||
getType(prop: FormProp): string { |
|||
switch (prop.type) { |
|||
case ePropType.Date: |
|||
case ePropType.String: |
|||
return 'text'; |
|||
case ePropType.Boolean: |
|||
return 'checkbox'; |
|||
case ePropType.Number: |
|||
return 'number'; |
|||
case ePropType.Email: |
|||
return 'email'; |
|||
case ePropType.Password: |
|||
return 'password'; |
|||
default: |
|||
return 'hidden'; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<ng-container *ngIf="form"> |
|||
<ng-container *abpPropData="let data; fromList: propList; withRecord: record"> |
|||
<ng-container *ngFor="let prop of propList; trackBy: track.by('name')"> |
|||
<ng-container *ngIf="prop.visible(data)"> |
|||
<ng-container |
|||
[formGroupName]="extraPropertiesKey" |
|||
*ngIf="extraProperties.controls[prop.name]; else tempDefault" |
|||
> |
|||
<abp-extensible-form-prop [prop]="prop" [data]="data"></abp-extensible-form-prop> |
|||
</ng-container> |
|||
|
|||
<ng-template #tempDefault> |
|||
<abp-extensible-form-prop |
|||
*ngIf="form.get(prop.name)" |
|||
[prop]="prop" |
|||
[data]="data" |
|||
></abp-extensible-form-prop> |
|||
</ng-template> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
@ -0,0 +1,82 @@ |
|||
import { TrackByService } from '@abp/ng.core'; |
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Inject, |
|||
Input, |
|||
OnDestroy, |
|||
Optional, |
|||
QueryList, |
|||
SkipSelf, |
|||
ViewChildren, |
|||
} from '@angular/core'; |
|||
import { ControlContainer, FormGroup } from '@angular/forms'; |
|||
import { Subscription } from 'rxjs'; |
|||
import { debounceTime } from 'rxjs/operators'; |
|||
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; |
|||
import { FormPropList } from '../../models/form-props'; |
|||
import { ExtensionsService } from '../../services/extensions.service'; |
|||
import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; |
|||
import { selfFactory } from '../../utils/factory.util'; |
|||
import { ExtensibleFormPropComponent } from './extensible-form-prop.component'; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpExtensibleForm', |
|||
selector: 'abp-extensible-form', |
|||
templateUrl: './extensible-form.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
viewProviders: [ |
|||
{ |
|||
provide: ControlContainer, |
|||
useFactory: selfFactory, |
|||
deps: [[new Optional(), new SkipSelf(), ControlContainer]], |
|||
}, |
|||
], |
|||
}) |
|||
export class ExtensibleFormComponent<R = any> implements AfterViewInit, OnDestroy { |
|||
@ViewChildren(ExtensibleFormPropComponent) |
|||
formProps: QueryList<ExtensibleFormPropComponent>; |
|||
|
|||
@Input() |
|||
set selectedRecord(record: R) { |
|||
const type = !record || JSON.stringify(record) === '{}' ? 'create' : 'edit'; |
|||
this.propList = this.extensions[`${type}FormProps`].get(this.identifier).props; |
|||
this.record = record; |
|||
} |
|||
|
|||
private subscription = new Subscription(); |
|||
extraPropertiesKey = EXTRA_PROPERTIES_KEY; |
|||
propList: FormPropList<R>; |
|||
record: R; |
|||
|
|||
get form(): FormGroup { |
|||
return (this.container ? this.container.control : { controls: {} }) as FormGroup; |
|||
} |
|||
|
|||
get extraProperties(): FormGroup { |
|||
return (this.form.controls.extraProperties || { controls: {} }) as FormGroup; |
|||
} |
|||
|
|||
constructor( |
|||
public readonly cdRef: ChangeDetectorRef, |
|||
public readonly track: TrackByService, |
|||
private container: ControlContainer, |
|||
private extensions: ExtensionsService, |
|||
@Inject(EXTENSIONS_IDENTIFIER) private identifier: string, |
|||
) {} |
|||
|
|||
ngAfterViewInit() { |
|||
this.subscription.add( |
|||
this.form.statusChanges.pipe(debounceTime(0)).subscribe(() => { |
|||
this.formProps.forEach(prop => prop.cdRef.markForCheck()); |
|||
this.cdRef.detectChanges(); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.subscription.unsubscribe(); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<ngx-datatable default [rows]="data" [count]="recordsTotal" [list]="list"> |
|||
<ngx-datatable-column |
|||
[name]="actionsText | abpLocalization" |
|||
[maxWidth]="columnWidths[0]" |
|||
[width]="columnWidths[0]" |
|||
[sortable]="false" |
|||
> |
|||
<ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template> |
|||
<ng-container |
|||
*ngTemplateOutlet="actionsTemplate || gridActions; context: { $implicit: row, index: i }" |
|||
></ng-container> |
|||
<ng-template #gridActions> |
|||
<abp-grid-actions |
|||
[index]="i" |
|||
[record]="row" |
|||
text="LanguageManagement::Actions" |
|||
></abp-grid-actions> |
|||
</ng-template> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
|
|||
<ng-container *ngFor="let prop of propList; let i = index; trackBy: trackByFn"> |
|||
<ngx-datatable-column |
|||
[width]="columnWidths[i + 1] || 200" |
|||
[name]="prop.displayName | abpLocalization" |
|||
[prop]="prop.name" |
|||
[sortable]="prop.sortable" |
|||
> |
|||
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template> |
|||
<ng-container |
|||
*abpPropData="let data; fromList: propList; withRecord: row; atIndex: i" |
|||
[abpPermission]="prop.permission" |
|||
> |
|||
<div *ngIf="prop.visible(data)" [innerHTML]="getContent(prop, data) | async"></div> |
|||
</ng-container> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
</ng-container> |
|||
</ngx-datatable> |
|||
@ -0,0 +1,88 @@ |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { formatDate } from '@angular/common'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
Inject, |
|||
Injector, |
|||
Input, |
|||
LOCALE_ID, |
|||
TemplateRef, |
|||
TrackByFunction, |
|||
} from '@angular/core'; |
|||
import { Observable } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { ePropType } from '../../enums/props.enum'; |
|||
import { EntityProp, EntityPropList } from '../../models/entity-props'; |
|||
import { PropData } from '../../models/props'; |
|||
import { ExtensionsService } from '../../services/extensions.service'; |
|||
import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; |
|||
const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpExtensibleTable', |
|||
selector: 'abp-extensible-table', |
|||
templateUrl: './extensible-table.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class ExtensibleTableComponent<R = any> { |
|||
@Input() actionsText: string; |
|||
@Input() data: R[]; |
|||
@Input() list: ListService; |
|||
@Input() recordsTotal: number; |
|||
@Input() set actionsColumnWidth(width: number) { |
|||
this.setColumnWidths(width ? Number(width) : undefined); |
|||
} |
|||
@Input() actionsTemplate: TemplateRef<any>; |
|||
|
|||
readonly columnWidths: number[]; |
|||
|
|||
readonly propList: EntityPropList<R>; |
|||
|
|||
readonly trackByFn: TrackByFunction<EntityProp<R>> = (_, item) => item.name; |
|||
|
|||
constructor(@Inject(LOCALE_ID) private locale: string, injector: Injector) { |
|||
const extensions = injector.get(ExtensionsService); |
|||
const name = injector.get(EXTENSIONS_IDENTIFIER); |
|||
this.propList = extensions.entityProps.get(name).props; |
|||
this.setColumnWidths(DEFAULT_ACTIONS_COLUMN_WIDTH); |
|||
} |
|||
|
|||
private setColumnWidths(actionsColumn: number) { |
|||
const widths = [actionsColumn]; |
|||
this.propList.forEach(({ value: prop }) => { |
|||
widths.push(prop.columnWidth); |
|||
}); |
|||
(this.columnWidths as any) = widths; |
|||
} |
|||
|
|||
private getDate(value: Date, format: string) { |
|||
return value ? formatDate(value, format, this.locale) : ''; |
|||
} |
|||
|
|||
private getIcon(value: boolean) { |
|||
return value |
|||
? '<div class="text-center text-success"><i class="fa fa-check"></i></div>' |
|||
: '<div class="text-center text-danger"><i class="fa fa-times"></i></div>'; |
|||
} |
|||
|
|||
getContent(prop: EntityProp<R>, data: PropData): Observable<string> { |
|||
return prop.valueResolver(data).pipe( |
|||
map(value => { |
|||
switch (prop.type) { |
|||
case ePropType.Boolean: |
|||
return this.getIcon(value); |
|||
case ePropType.Date: |
|||
return this.getDate(value, 'yyyy-MM-dd'); |
|||
case ePropType.Time: |
|||
return this.getDate(value, 'HH:mm'); |
|||
case ePropType.DateTime: |
|||
return this.getDate(value, 'yyyy-MM-dd HH:mm:ss Z'); |
|||
default: |
|||
return value; |
|||
// More types can be handled in the future
|
|||
} |
|||
}), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<div ngbDropdown container="body" class="d-inline-block"> |
|||
<button |
|||
class="btn btn-primary btn-sm dropdown-toggle" |
|||
data-toggle="dropdown" |
|||
aria-haspopup="true" |
|||
ngbDropdownToggle |
|||
> |
|||
<i [ngClass]="icon" [class.mr-1]="icon"></i>{{ text | abpLocalization }} |
|||
</button> |
|||
<div ngbDropdownMenu> |
|||
<ng-container *ngFor="let action of actionList; trackBy: trackByFn"> |
|||
<button |
|||
*ngIf="action.visible(data)" |
|||
ngbDropdownItem |
|||
[abpPermission]="action.permission" |
|||
(click)="action.action(data)" |
|||
type="button" |
|||
> |
|||
<i [ngClass]="action.icon" [class.mr-1]="action.icon"></i> |
|||
{{ action.text | abpLocalization }} |
|||
</button> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,36 @@ |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
Injector, |
|||
Input, |
|||
TrackByFunction, |
|||
} from '@angular/core'; |
|||
import { EntityAction, EntityActionList } from '../../models/entity-actions'; |
|||
import { EXTENSIONS_ACTION_TYPE } from '../../tokens/extensions.token'; |
|||
import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpGridActions', |
|||
selector: 'abp-grid-actions', |
|||
templateUrl: './grid-actions.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: EXTENSIONS_ACTION_TYPE, |
|||
useValue: 'entityActions', |
|||
}, |
|||
], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class GridActionsComponent<R = any> extends AbstractActionsComponent<EntityActionList<R>> { |
|||
@Input() icon = 'fa fa-cog'; |
|||
|
|||
@Input() readonly index: number; |
|||
|
|||
@Input() text = ''; |
|||
|
|||
readonly trackByFn: TrackByFunction<EntityAction<R>> = (_, item) => item.text; |
|||
|
|||
constructor(injector: Injector) { |
|||
super(injector); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<div class="row justify-content-end mx-n1" id="AbpContentToolbar"> |
|||
<div class="col-auto px-1 pt-2" *ngFor="let action of actionList; trackBy: trackByFn"> |
|||
<ng-container *ngIf="action.visible(data)" [abpPermission]="action.permission"> |
|||
<ng-container *ngIf="action.component as component; else button"> |
|||
<ng-container |
|||
*ngComponentOutlet="component; injector: createInjector(action)" |
|||
></ng-container> |
|||
</ng-container> |
|||
|
|||
<ng-template #button> |
|||
<button (click)="action.action(data)" type="button" class="btn btn-primary btn-sm"> |
|||
<i [ngClass]="action.icon" [class.mr-1]="action.icon"></i> |
|||
{{ action.text | abpLocalization }} |
|||
</button> |
|||
</ng-template> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,49 @@ |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
InjectFlags, |
|||
InjectionToken, |
|||
Injector, |
|||
TrackByFunction, |
|||
Type, |
|||
} from '@angular/core'; |
|||
import { ToolbarActionList, ToolbarComponent } from '../../models/toolbar-actions'; |
|||
import { |
|||
EXTENSIONS_ACTION_CALLBACK, |
|||
EXTENSIONS_ACTION_DATA, |
|||
EXTENSIONS_ACTION_TYPE, |
|||
} from '../../tokens/extensions.token'; |
|||
import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpPageToolbar', |
|||
selector: 'abp-page-toolbar', |
|||
templateUrl: './page-toolbar.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: EXTENSIONS_ACTION_TYPE, |
|||
useValue: 'toolbarActions', |
|||
}, |
|||
], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class PageToolbarComponent<R = any> extends AbstractActionsComponent<ToolbarActionList<R>> { |
|||
readonly trackByFn: TrackByFunction<ToolbarComponent<R>> = (_, item) => |
|||
item.action || item.component; |
|||
|
|||
constructor(private readonly injector: Injector) { |
|||
super(injector); |
|||
} |
|||
|
|||
createInjector(action: ToolbarComponent<R>): Injector { |
|||
const get = <T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags) => { |
|||
return token === EXTENSIONS_ACTION_DATA |
|||
? this.data |
|||
: token === EXTENSIONS_ACTION_CALLBACK |
|||
? (data = this.data) => action.action(data) |
|||
: this.getInjected.call(this.injector, token, notFoundValue, flags); |
|||
}; |
|||
|
|||
return { get }; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export const EXTRA_PROPERTIES_KEY = 'extraProperties'; |
|||
@ -0,0 +1,19 @@ |
|||
import { Directive, Host, Input, OnChanges, SimpleChanges } from '@angular/core'; |
|||
import { NgControl } from '@angular/forms'; |
|||
|
|||
@Directive({ |
|||
selector: '[abpDisabled]', |
|||
}) |
|||
export class DisabledDirective implements OnChanges { |
|||
@Input() |
|||
abpDisabled: boolean; |
|||
|
|||
constructor(@Host() private ngControl: NgControl) {} |
|||
|
|||
// Related issue: https://github.com/angular/angular/issues/35330
|
|||
ngOnChanges({ abpDisabled }: SimpleChanges) { |
|||
if (this.ngControl.control && abpDisabled) { |
|||
this.ngControl.control[abpDisabled.currentValue ? 'disable' : 'enable'](); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
import { |
|||
Directive, |
|||
Injector, |
|||
Input, |
|||
OnChanges, |
|||
OnDestroy, |
|||
TemplateRef, |
|||
ViewContainerRef, |
|||
} from '@angular/core'; |
|||
import { PropData, PropList } from '../models/props'; |
|||
|
|||
@Directive({ |
|||
exportAs: 'abpPropData', |
|||
selector: '[abpPropData]', |
|||
}) |
|||
export class PropDataDirective<L extends PropList<any>> extends PropData<InferredData<L>> |
|||
implements OnChanges, OnDestroy { |
|||
/* tslint:disable:no-input-rename */ |
|||
@Input('abpPropDataFromList') readonly propList: L; |
|||
|
|||
@Input('abpPropDataWithRecord') readonly record: InferredData<L>['record']; |
|||
|
|||
@Input('abpPropDataAtIndex') readonly index: number; |
|||
/* tslint:enable:no-input-rename */ |
|||
|
|||
readonly getInjected: InferredData<L>['getInjected']; |
|||
|
|||
constructor( |
|||
private tempRef: TemplateRef<any>, |
|||
private vcRef: ViewContainerRef, |
|||
injector: Injector, |
|||
) { |
|||
super(); |
|||
|
|||
// tslint:disable-next-line
|
|||
this.getInjected = injector.get.bind(injector); |
|||
} |
|||
|
|||
ngOnChanges() { |
|||
this.vcRef.clear(); |
|||
|
|||
this.vcRef.createEmbeddedView(this.tempRef, { |
|||
$implicit: this.data, |
|||
index: 0, |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.vcRef.clear(); |
|||
} |
|||
} |
|||
|
|||
type InferredData<L> = PropData<InferredRecord<L>>; |
|||
type InferredRecord<L> = L extends PropList<infer R> ? R : never; |
|||
@ -0,0 +1,12 @@ |
|||
export const enum ePropType { |
|||
Boolean = 'boolean', |
|||
Date = 'date', |
|||
DateTime = 'dateTime', |
|||
Email = 'email', |
|||
Enum = 'enum', |
|||
Number = 'number', |
|||
Password = 'password', |
|||
String = 'string', |
|||
Text = 'text', |
|||
Time = 'time', |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { InjectFlags, InjectionToken, Type } from '@angular/core'; |
|||
import { O } from 'ts-toolbelt'; |
|||
|
|||
export abstract class ActionList<R = any, A = Action<R>> extends LinkedList<A> {} |
|||
|
|||
export abstract class ActionData<R = any> { |
|||
abstract getInjected: <T>( |
|||
token: Type<T> | InjectionToken<T>, |
|||
notFoundValue?: T, |
|||
flags?: InjectFlags, |
|||
) => T; |
|||
index?: number; |
|||
abstract record: R; |
|||
|
|||
get data(): ReadonlyActionData<R> { |
|||
return { |
|||
getInjected: this.getInjected, |
|||
index: this.index, |
|||
record: this.record, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
export type ReadonlyActionData<R = any> = O.Readonly<Omit<ActionData<R>, 'data'>>; |
|||
|
|||
export abstract class Action<R = any> { |
|||
constructor( |
|||
public readonly permission: string, |
|||
public readonly visible: ActionPredicate<R> = _ => true, |
|||
public readonly action: ActionCallback<R> = _ => {}, |
|||
) {} |
|||
} |
|||
|
|||
export type ActionCallback<T, R = any> = (data?: Omit<ActionData<T>, 'data'>) => R; |
|||
export type ActionPredicate<T> = (data?: Omit<ActionData<T>, 'data'>) => boolean; |
|||
|
|||
export abstract class ActionsFactory<C extends Actions<any>> { |
|||
protected abstract _ctor: Type<C>; |
|||
private contributorCallbacks: ActionContributorCallbacks<InferredActionList<C>> = {}; |
|||
|
|||
get(name: string): C { |
|||
this.contributorCallbacks[name] = this.contributorCallbacks[name] || []; |
|||
|
|||
return new this._ctor(this.contributorCallbacks[name]); |
|||
} |
|||
} |
|||
|
|||
export abstract class Actions<L extends ActionList> { |
|||
protected abstract _ctor: Type<L>; |
|||
|
|||
get actions(): L { |
|||
const actionList = new this._ctor(); |
|||
|
|||
this.callbackList.forEach(callback => callback(actionList)); |
|||
|
|||
return actionList; |
|||
} |
|||
|
|||
constructor(private readonly callbackList: ActionContributorCallback<L>[]) {} |
|||
|
|||
addContributor(contributeCallback: ActionContributorCallback<L>) { |
|||
this.callbackList.push(contributeCallback); |
|||
} |
|||
|
|||
clearContributors() { |
|||
while (this.callbackList.length) this.callbackList.pop(); |
|||
} |
|||
} |
|||
|
|||
export type ActionContributorCallbacks<L extends ActionList<any>> = Record< |
|||
string, |
|||
ActionContributorCallback<L>[] |
|||
>; |
|||
|
|||
export type ActionContributorCallback<L extends ActionList<any>> = (actionList: L) => any; |
|||
|
|||
type InferredActionList<C> = C extends Actions<infer L> ? L : never; |
|||
@ -0,0 +1,53 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { Type } from '@angular/core'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { |
|||
Action, |
|||
ActionContributorCallback, |
|||
ActionContributorCallbacks, |
|||
ActionList, |
|||
Actions, |
|||
ActionsFactory, |
|||
} from './actions'; |
|||
|
|||
export class EntityActionList<R = any> extends ActionList<R, EntityAction<R>> {} |
|||
|
|||
export class EntityActions<R = any> extends Actions<EntityActionList<R>> { |
|||
protected _ctor: Type<EntityActionList<R>> = EntityActionList; |
|||
} |
|||
|
|||
export class EntityActionsFactory<R = any> extends ActionsFactory<EntityActions<R>> { |
|||
protected _ctor: Type<EntityActions<R>> = EntityActions; |
|||
} |
|||
|
|||
export class EntityAction<R = any> extends Action<R> { |
|||
readonly text: string; |
|||
readonly icon: string; |
|||
|
|||
constructor(options: EntityActionOptions<R>) { |
|||
super(options.permission, options.visible, options.action); |
|||
this.text = options.text; |
|||
this.icon = options.icon || ''; |
|||
} |
|||
|
|||
static create<R = any>(options: EntityActionOptions<R>) { |
|||
return new EntityAction<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: EntityActionOptions<R>[]) { |
|||
return arrayOfOptions.map(EntityAction.create); |
|||
} |
|||
} |
|||
|
|||
export type EntityActionOptions<R = any> = O.Optional< |
|||
O.Writable<EntityAction<R>>, |
|||
'permission' | 'visible' | 'icon' |
|||
>; |
|||
|
|||
export type EntityActionDefaults<R = any> = Record<string, EntityAction<R>[]>; |
|||
export type EntityActionContributorCallback<R = any> = ActionContributorCallback< |
|||
EntityActionList<R> |
|||
>; |
|||
export type EntityActionContributorCallbacks<R = any> = ActionContributorCallbacks< |
|||
EntityActionList<R> |
|||
>; |
|||
@ -0,0 +1,67 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { Type } from '@angular/core'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { |
|||
Prop, |
|||
PropCallback, |
|||
PropContributorCallback, |
|||
PropContributorCallbacks, |
|||
PropList, |
|||
Props, |
|||
PropsFactory, |
|||
} from './props'; |
|||
|
|||
export class EntityPropList<R = any> extends PropList<R, EntityProp<R>> {} |
|||
|
|||
export class EntityProps<R = any> extends Props<EntityPropList<R>> { |
|||
protected _ctor: Type<EntityPropList<R>> = EntityPropList; |
|||
} |
|||
|
|||
export class EntityPropsFactory<R = any> extends PropsFactory<EntityProps<R>> { |
|||
protected _ctor: Type<EntityProps<R>> = EntityProps; |
|||
} |
|||
|
|||
export class EntityProp<R = any> extends Prop<R> { |
|||
readonly columnWidth: number | undefined; |
|||
readonly sortable: boolean; |
|||
readonly valueResolver: PropCallback<R, Observable<any>>; |
|||
|
|||
constructor(options: EntityPropOptions<R>) { |
|||
super( |
|||
options.type, |
|||
options.name, |
|||
options.displayName, |
|||
options.permission, |
|||
options.visible, |
|||
options.isExtra, |
|||
); |
|||
|
|||
this.columnWidth = options.columnWidth; |
|||
this.sortable = options.sortable || false; |
|||
this.valueResolver = options.valueResolver || (data => of(data.record[this.name])); |
|||
} |
|||
|
|||
static create<R = any>(options: EntityPropOptions<R>) { |
|||
return new EntityProp<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: EntityPropOptions<R>[]) { |
|||
return arrayOfOptions.map(EntityProp.create); |
|||
} |
|||
} |
|||
|
|||
export type EntityPropOptions<R = any> = O.Optional< |
|||
O.Writable<EntityProp<R>>, |
|||
| 'permission' |
|||
| 'visible' |
|||
| 'displayName' |
|||
| 'isExtra' |
|||
| 'columnWidth' |
|||
| 'sortable' |
|||
| 'valueResolver' |
|||
>; |
|||
|
|||
export type EntityPropDefaults<R = any> = Record<string, EntityProp<R>[]>; |
|||
export type EntityPropContributorCallback<R = any> = PropContributorCallback<EntityPropList<R>>; |
|||
export type EntityPropContributorCallbacks<R = any> = PropContributorCallbacks<EntityPropList<R>>; |
|||
@ -0,0 +1,109 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { ABP } from '@abp/ng.core'; |
|||
import { Injector, Type } from '@angular/core'; |
|||
import { AsyncValidatorFn, ValidatorFn } from '@angular/forms'; |
|||
import { Observable } from 'rxjs'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { |
|||
Prop, |
|||
PropCallback, |
|||
PropContributorCallback, |
|||
PropContributorCallbacks, |
|||
PropData, |
|||
PropList, |
|||
PropPredicate, |
|||
Props, |
|||
PropsFactory, |
|||
} from './props'; |
|||
|
|||
export class FormPropList<R = any> extends PropList<R, FormProp<R>> {} |
|||
|
|||
export class FormProps<R = any> extends Props<FormPropList<R>> { |
|||
protected _ctor: Type<FormPropList<R>> = FormPropList; |
|||
} |
|||
|
|||
export class CreateFormPropsFactory<R = any> extends PropsFactory<FormProps<R>> { |
|||
protected _ctor: Type<FormProps<R>> = FormProps; |
|||
} |
|||
|
|||
export class EditFormPropsFactory<R = any> extends PropsFactory<FormProps<R>> { |
|||
protected _ctor: Type<FormProps<R>> = FormProps; |
|||
} |
|||
|
|||
export class FormProp<R = any> extends Prop<R> { |
|||
readonly validators: PropCallback<R, ValidatorFn[]>; |
|||
readonly asyncValidators: PropCallback<R, AsyncValidatorFn[]>; |
|||
readonly disabled: PropPredicate<R>; |
|||
readonly readonly: PropPredicate<R>; |
|||
readonly autocomplete: string; |
|||
readonly defaultValue: boolean | number | string | Date; |
|||
readonly options: PropCallback<R, Observable<ABP.Option<any>[]>> | undefined; |
|||
readonly id: string | undefined; |
|||
|
|||
constructor(options: FormPropOptions<R>) { |
|||
super( |
|||
options.type, |
|||
options.name, |
|||
options.displayName, |
|||
options.permission, |
|||
options.visible, |
|||
options.isExtra, |
|||
); |
|||
|
|||
this.asyncValidators = options.asyncValidators || (_ => []); |
|||
this.validators = options.validators || (_ => []); |
|||
this.disabled = options.disabled || (_ => false); |
|||
this.readonly = options.readonly || (_ => false); |
|||
this.autocomplete = options.autocomplete || 'off'; |
|||
this.options = options.options; |
|||
this.id = options.id || options.name; |
|||
const defaultValue = options.defaultValue; |
|||
this.defaultValue = isFalsyValue(defaultValue) ? defaultValue : defaultValue || null; |
|||
} |
|||
|
|||
static create<R = any>(options: FormPropOptions<R>) { |
|||
return new FormProp<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: FormPropOptions<R>[]) { |
|||
return arrayOfOptions.map(FormProp.create); |
|||
} |
|||
} |
|||
|
|||
export class FormPropData<R = any> extends PropData<R> { |
|||
getInjected: PropData<R>['getInjected']; |
|||
|
|||
constructor(injector: Injector, public readonly record: R) { |
|||
super(); |
|||
|
|||
// tslint:disable-next-line
|
|||
this.getInjected = injector.get.bind(injector); |
|||
} |
|||
} |
|||
|
|||
export type FormPropOptions<R = any> = O.Optional< |
|||
O.Writable<FormProp<R>>, |
|||
| 'permission' |
|||
| 'visible' |
|||
| 'displayName' |
|||
| 'isExtra' |
|||
| 'validators' |
|||
| 'asyncValidators' |
|||
| 'disabled' |
|||
| 'readonly' |
|||
| 'autocomplete' |
|||
| 'defaultValue' |
|||
| 'options' |
|||
| 'id' |
|||
>; |
|||
|
|||
export type CreateFormPropDefaults<R = any> = Record<string, FormProp<R>[]>; |
|||
export type CreateFormPropContributorCallback<R = any> = PropContributorCallback<FormPropList<R>>; |
|||
export type CreateFormPropContributorCallbacks<R = any> = PropContributorCallbacks<FormPropList<R>>; |
|||
export type EditFormPropDefaults<R = any> = Record<string, FormProp<R>[]>; |
|||
export type EditFormPropContributorCallback<R = any> = PropContributorCallback<FormPropList<R>>; |
|||
export type EditFormPropContributorCallbacks<R = any> = PropContributorCallbacks<FormPropList<R>>; |
|||
|
|||
function isFalsyValue(defaultValue: FormProp['defaultValue']): boolean { |
|||
return [0, '', false].indexOf(defaultValue as any) > -1; |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
import { ePropType } from '../../enums/props.enum'; |
|||
import { EntityPropList } from '../entity-props'; |
|||
import { FormPropList } from '../form-props'; |
|||
import { PropContributorCallbacks } from '../props'; |
|||
|
|||
export interface PropContributors<T = any> { |
|||
prop: PropContributorCallbacks<EntityPropList<T>>; |
|||
createForm: PropContributorCallbacks<FormPropList<T>>; |
|||
editForm: PropContributorCallbacks<FormPropList<T>>; |
|||
} |
|||
|
|||
export interface Config { |
|||
objectExtensions: Item; |
|||
} |
|||
|
|||
export interface Item { |
|||
modules: Modules; |
|||
enums: Enums; |
|||
} |
|||
|
|||
export type Modules = Configuration<Module>; |
|||
|
|||
export interface Module { |
|||
configuration: Configuration; |
|||
entities: Entities; |
|||
} |
|||
|
|||
export type Entities = Configuration<Entity>; |
|||
|
|||
export interface Entity { |
|||
configuration: Configuration; |
|||
properties: Properties; |
|||
} |
|||
|
|||
export type Properties = Configuration<Property>; |
|||
|
|||
export interface Property { |
|||
type: string; |
|||
typeSimple: ePropType; |
|||
displayName: DisplayName | null; |
|||
api: Api; |
|||
ui: Ui; |
|||
attributes: Attribute[]; |
|||
configuration: Configuration; |
|||
defaultValue?: any; |
|||
} |
|||
|
|||
export interface DisplayName { |
|||
name: string; |
|||
resource: string; |
|||
} |
|||
|
|||
export interface Api { |
|||
onGet: ApiConfig; |
|||
onCreate: ApiConfig; |
|||
onUpdate: ApiConfig; |
|||
} |
|||
|
|||
export interface ApiConfig { |
|||
isAvailable: boolean; |
|||
} |
|||
|
|||
export interface Ui { |
|||
onTable: UiPropConfig; |
|||
onCreateForm: UiFormConfig; |
|||
onEditForm: UiFormConfig; |
|||
} |
|||
|
|||
export interface UiPropConfig { |
|||
isSortable: boolean; |
|||
isVisible: boolean; |
|||
} |
|||
|
|||
export interface UiFormConfig { |
|||
isVisible: boolean; |
|||
} |
|||
|
|||
export interface Attribute { |
|||
typeSimple: string; |
|||
config: Configuration; |
|||
} |
|||
|
|||
export type Configuration<T = any> = Record<string, T>; |
|||
|
|||
export type Enums = Record<string, Enum>; |
|||
|
|||
export interface Enum { |
|||
fields: EnumMember[]; |
|||
localizationResource?: string; |
|||
transformed?: any; |
|||
} |
|||
|
|||
export interface EnumMember { |
|||
name: string; |
|||
value: any; |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
import * as ObjectExtensions from './internal/object-extensions'; |
|||
|
|||
export { ObjectExtensions }; |
|||
@ -0,0 +1,85 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { InjectFlags, InjectionToken, Type } from '@angular/core'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { ePropType } from '../enums/props.enum'; |
|||
|
|||
export abstract class PropList<R = any, A = Prop<R>> extends LinkedList<A> {} |
|||
|
|||
export abstract class PropData<R = any> { |
|||
abstract getInjected: <T>( |
|||
token: Type<T> | InjectionToken<T>, |
|||
notFoundValue?: T, |
|||
flags?: InjectFlags, |
|||
) => T; |
|||
index?: number; |
|||
abstract record: R; |
|||
|
|||
get data(): ReadonlyPropData<R> { |
|||
return { |
|||
getInjected: this.getInjected, |
|||
index: this.index, |
|||
record: this.record, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
export type ReadonlyPropData<R = any> = O.Readonly<Omit<PropData<R>, 'data'>>; |
|||
|
|||
export abstract class Prop<R = any> { |
|||
constructor( |
|||
public readonly type: ePropType, |
|||
public readonly name: string, |
|||
public readonly displayName: string, |
|||
public readonly permission: string, |
|||
public readonly visible: PropPredicate<R> = _ => true, |
|||
public readonly isExtra = false, |
|||
) { |
|||
this.displayName = this.displayName || this.name; |
|||
} |
|||
} |
|||
|
|||
export type PropCallback<T, R = any> = (data?: Omit<PropData<T>, 'data'>) => R; |
|||
export type PropPredicate<T> = (data?: Omit<PropData<T>, 'data'>) => boolean; |
|||
|
|||
export abstract class PropsFactory<C extends Props<any>> { |
|||
protected abstract _ctor: Type<C>; |
|||
private contributorCallbacks: PropContributorCallbacks<InferredPropList<C>> = {}; |
|||
|
|||
get(name: string): C { |
|||
this.contributorCallbacks[name] = this.contributorCallbacks[name] || []; |
|||
|
|||
return new this._ctor(this.contributorCallbacks[name]); |
|||
} |
|||
} |
|||
|
|||
export abstract class Props<L extends PropList> { |
|||
protected abstract _ctor: Type<L>; |
|||
|
|||
get props(): L { |
|||
const propList = new this._ctor(); |
|||
|
|||
this.callbackList.forEach(callback => callback(propList)); |
|||
|
|||
return propList; |
|||
} |
|||
|
|||
constructor(private readonly callbackList: PropContributorCallback<L>[]) {} |
|||
|
|||
addContributor(contributeCallback: PropContributorCallback<L>) { |
|||
this.callbackList.push(contributeCallback); |
|||
} |
|||
|
|||
clearContributors() { |
|||
while (this.callbackList.length) this.callbackList.pop(); |
|||
} |
|||
} |
|||
|
|||
export type PropContributorCallbacks<L extends PropList<any>> = Record< |
|||
string, |
|||
PropContributorCallback<L>[] |
|||
>; |
|||
|
|||
export type PropContributorCallback<L extends PropList<any>> = (propList: L) => any; |
|||
|
|||
type InferredPropList<C> = C extends Props<infer L> ? L : never; |
|||
@ -0,0 +1,78 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { Type } from '@angular/core'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { |
|||
Action, |
|||
ActionContributorCallback, |
|||
ActionContributorCallbacks, |
|||
ActionList, |
|||
Actions, |
|||
ActionsFactory, |
|||
} from './actions'; |
|||
|
|||
export class ToolbarActionList<R = any> extends ActionList< |
|||
R, |
|||
ToolbarAction<R> | ToolbarComponent<R> |
|||
> {} |
|||
|
|||
export class ToolbarActions<R = any> extends Actions<ToolbarActionList<R>> { |
|||
protected _ctor: Type<ToolbarActionList<R>> = ToolbarActionList; |
|||
} |
|||
|
|||
export class ToolbarActionsFactory<R = any> extends ActionsFactory<ToolbarActions<R>> { |
|||
protected _ctor: Type<ToolbarActions<R>> = ToolbarActions; |
|||
} |
|||
|
|||
export class ToolbarAction<R = any> extends Action<R> { |
|||
readonly text: string; |
|||
readonly icon: string; |
|||
|
|||
constructor(options: ToolbarActionOptions<R>) { |
|||
super(options.permission, options.visible, options.action); |
|||
this.text = options.text; |
|||
this.icon = options.icon || ''; |
|||
} |
|||
|
|||
static create<R = any>(options: ToolbarActionOptions<R>) { |
|||
return new ToolbarAction<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: ToolbarActionOptions<R>[]) { |
|||
return arrayOfOptions.map(ToolbarAction.create); |
|||
} |
|||
} |
|||
|
|||
export class ToolbarComponent<R = any> extends Action<R> { |
|||
readonly component: Type<any>; |
|||
|
|||
constructor(options: ToolbarComponentOptions<R>) { |
|||
super(options.permission, options.visible, options.action); |
|||
this.component = options.component; |
|||
} |
|||
|
|||
static create<R = any>(options: ToolbarComponentOptions<R>) { |
|||
return new ToolbarComponent<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: ToolbarComponentOptions<R>[]) { |
|||
return arrayOfOptions.map(ToolbarComponent.create); |
|||
} |
|||
} |
|||
|
|||
export type ToolbarActionOptions<R = any> = O.Optional< |
|||
O.Writable<ToolbarAction<R>>, |
|||
'permission' | 'visible' | 'icon' |
|||
>; |
|||
|
|||
export type ToolbarComponentOptions<R = any> = O.Optional< |
|||
O.Writable<ToolbarComponent<R>>, |
|||
'permission' | 'visible' | 'action' |
|||
>; |
|||
|
|||
export type ToolbarActionDefaults<R = any> = Record<string, ToolbarAction<R>[]>; |
|||
export type ToolbarActionContributorCallback<R = any> = ActionContributorCallback< |
|||
ToolbarActionList<R> |
|||
>; |
|||
export type ToolbarActionContributorCallbacks<R = any> = ActionContributorCallbacks< |
|||
ToolbarActionList<R> |
|||
>; |
|||
@ -0,0 +1,16 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { EntityActionsFactory } from '../models/entity-actions'; |
|||
import { EntityPropsFactory } from '../models/entity-props'; |
|||
import { CreateFormPropsFactory, EditFormPropsFactory } from '../models/form-props'; |
|||
import { ToolbarActionsFactory } from '../models/toolbar-actions'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class ExtensionsService<R = any> { |
|||
readonly entityActions = new EntityActionsFactory<R>(); |
|||
readonly toolbarActions = new ToolbarActionsFactory<R[]>(); |
|||
readonly entityProps = new EntityPropsFactory<R>(); |
|||
readonly createFormProps = new CreateFormPropsFactory<R>(); |
|||
readonly editFormProps = new EditFormPropsFactory<R>(); |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
import { InjectionToken } from '@angular/core'; |
|||
import { ActionCallback, ReadonlyActionData as ActionData } from '../models/actions'; |
|||
import { ExtensionsService } from '../services/extensions.service'; |
|||
|
|||
export const EXTENSIONS_IDENTIFIER = new InjectionToken<string>('EXTENSIONS_IDENTIFIER'); |
|||
export type ActionKeys = Extract<'entityActions' | 'toolbarActions', keyof ExtensionsService>; |
|||
|
|||
export const EXTENSIONS_ACTION_TYPE = new InjectionToken<ActionKeys>('EXTENSIONS_ACTION_TYPE'); |
|||
|
|||
export const EXTENSIONS_ACTION_DATA = new InjectionToken<ActionData>('EXTENSIONS_ACTION_DATA'); |
|||
export const EXTENSIONS_ACTION_CALLBACK = new InjectionToken<ActionCallback<unknown>>( |
|||
'EXTENSIONS_ACTION_DATA', |
|||
); |
|||
@ -0,0 +1,48 @@ |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { |
|||
NgbDatepickerModule, |
|||
NgbDropdownModule, |
|||
NgbTimepickerModule, |
|||
} from '@ng-bootstrap/ng-bootstrap'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { DateTimePickerComponent } from './components/date-time-picker/date-time-picker.component'; |
|||
import { ExtensibleFormPropComponent } from './components/extensible-form/extensible-form-prop.component'; |
|||
import { ExtensibleFormComponent } from './components/extensible-form/extensible-form.component'; |
|||
import { ExtensibleTableComponent } from './components/extensible-table/extensible-table.component'; |
|||
import { GridActionsComponent } from './components/grid-actions/grid-actions.component'; |
|||
import { PageToolbarComponent } from './components/page-toolbar/page-toolbar.component'; |
|||
import { DisabledDirective } from './directives/disabled.directive'; |
|||
import { PropDataDirective } from './directives/prop-data.directive'; |
|||
|
|||
@NgModule({ |
|||
exports: [ |
|||
DateTimePickerComponent, |
|||
PageToolbarComponent, |
|||
GridActionsComponent, |
|||
ExtensibleFormComponent, |
|||
ExtensibleTableComponent, |
|||
PropDataDirective, |
|||
DisabledDirective, |
|||
], |
|||
declarations: [ |
|||
DateTimePickerComponent, |
|||
PageToolbarComponent, |
|||
GridActionsComponent, |
|||
ExtensibleFormPropComponent, |
|||
ExtensibleFormComponent, |
|||
ExtensibleTableComponent, |
|||
PropDataDirective, |
|||
DisabledDirective, |
|||
], |
|||
imports: [ |
|||
CoreModule, |
|||
ThemeSharedModule, |
|||
NgxValidateCoreModule, |
|||
NgbDatepickerModule, |
|||
NgbDropdownModule, |
|||
NgbTimepickerModule, |
|||
], |
|||
}) |
|||
export class UiExtensionsModule {} |
|||
@ -0,0 +1,49 @@ |
|||
import { ActionContributorCallback, ActionList, ActionsFactory } from '../models/actions'; |
|||
import { |
|||
EntityActionContributorCallbacks, |
|||
EntityActionDefaults, |
|||
EntityActions, |
|||
EntityActionsFactory, |
|||
} from '../models/entity-actions'; |
|||
import { |
|||
ToolbarActionContributorCallbacks, |
|||
ToolbarActionDefaults, |
|||
ToolbarActions, |
|||
ToolbarActionsFactory, |
|||
} from '../models/toolbar-actions'; |
|||
|
|||
export function mergeWithDefaultActions<F extends ActionsFactory<any>>( |
|||
extension: F, |
|||
defaultActions: InferredActionDefaults<F>, |
|||
...contributors: InferredActionContributorCallbacks<F>[] |
|||
) { |
|||
Object.keys(defaultActions).forEach((name: string) => { |
|||
const actions: InferredActions<F> = extension.get(name); |
|||
actions.clearContributors(); |
|||
actions.addContributor((actionList: ActionList) => |
|||
actionList.addManyTail(defaultActions[name]), |
|||
); |
|||
contributors.forEach(contributor => |
|||
(contributor[name] || []).forEach((callback: ActionContributorCallback<any>) => |
|||
actions.addContributor(callback), |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
type InferredActionDefaults<F> = F extends EntityActionsFactory<infer RE> |
|||
? EntityActionDefaults<RE> |
|||
: F extends ToolbarActionsFactory<infer RT> |
|||
? ToolbarActionDefaults<RT> |
|||
: never; |
|||
|
|||
type InferredActionContributorCallbacks<F> = F extends EntityActionsFactory<infer RE> |
|||
? EntityActionContributorCallbacks<RE> |
|||
: F extends ToolbarActionsFactory<infer RT> |
|||
? ToolbarActionContributorCallbacks<RT> |
|||
: never; |
|||
|
|||
type InferredActions<F> = F extends EntityActionsFactory<infer RE> |
|||
? EntityActions<RE> |
|||
: F extends ToolbarActionsFactory<infer RT> |
|||
? ToolbarActions<RT> |
|||
: never; |
|||
@ -0,0 +1,73 @@ |
|||
import { ABP, LocalizationService } from '@abp/ng.core'; |
|||
import { merge, Observable, of } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
import { PropCallback } from '../models/props'; |
|||
|
|||
export function createEnum(members: ObjectExtensions.EnumMember[]) { |
|||
const enumObject: any = {}; |
|||
|
|||
members.forEach(({ name, value }) => { |
|||
enumObject[(enumObject[name] = value)] = name; |
|||
}); |
|||
|
|||
return enumObject; |
|||
} |
|||
|
|||
export function createEnumValueResolver<T = any>( |
|||
enumType: string, |
|||
lookupEnum: ObjectExtensions.Enum, |
|||
propName: string, |
|||
): PropCallback<T, Observable<string>> { |
|||
return data => { |
|||
const value = data.record[EXTRA_PROPERTIES_KEY][propName]; |
|||
const key = lookupEnum.transformed[value]; |
|||
const l10n = data.getInjected(LocalizationService); |
|||
const localizeEnum = createEnumLocalizer(l10n, enumType, lookupEnum); |
|||
|
|||
return createLocalizationStream(l10n, localizeEnum(key)); |
|||
}; |
|||
} |
|||
|
|||
export function createEnumOptions<T = any>( |
|||
enumType: string, |
|||
lookupEnum: ObjectExtensions.Enum, |
|||
): PropCallback<T, Observable<ABP.Option<any>[]>> { |
|||
return data => { |
|||
const l10n = data.getInjected(LocalizationService); |
|||
const localizeEnum = createEnumLocalizer(l10n, enumType, lookupEnum); |
|||
|
|||
return createLocalizationStream( |
|||
l10n, |
|||
lookupEnum.fields.map(({ name, value }) => ({ |
|||
key: localizeEnum(name), |
|||
value, |
|||
})), |
|||
); |
|||
}; |
|||
} |
|||
|
|||
function createLocalizationStream(l10n: LocalizationService, mapTarget: any) { |
|||
return merge(of(null), l10n.languageChange).pipe(map(() => mapTarget)); |
|||
} |
|||
|
|||
function createEnumLocalizer( |
|||
l10n: LocalizationService, |
|||
enumType: string, |
|||
lookupEnum: ObjectExtensions.Enum, |
|||
): (key: string) => string { |
|||
const resource = lookupEnum.localizationResource; |
|||
const shortType = getShortEnumType(enumType); |
|||
|
|||
return key => |
|||
l10n.localizeWithFallbackSync( |
|||
[resource], |
|||
['Enum:' + shortType + '.' + key, shortType + '.' + key, key], |
|||
key, |
|||
); |
|||
} |
|||
|
|||
function getShortEnumType(enumType: string): string { |
|||
return enumType.split('.').pop(); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export function selfFactory(dependency?: any) { |
|||
return dependency; |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
import { FormControl, FormGroup } from '@angular/forms'; |
|||
import { DateTimeAdapter } from '../adapters/date-time.adapter'; |
|||
import { DateAdapter } from '../adapters/date.adapter'; |
|||
import { TimeAdapter } from '../adapters/time.adapter'; |
|||
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; |
|||
import { ePropType } from '../enums/props.enum'; |
|||
import { FormPropList } from '../models/form-props'; |
|||
import { PropData } from '../models/props'; |
|||
import { ExtensionsService } from '../services/extensions.service'; |
|||
import { EXTENSIONS_IDENTIFIER } from '../tokens/extensions.token'; |
|||
|
|||
export function generateFormFromProps<R extends any>(data: PropData<R>) { |
|||
const extensions = data.getInjected(ExtensionsService); |
|||
const identifier = data.getInjected(EXTENSIONS_IDENTIFIER); |
|||
|
|||
const form = new FormGroup({}); |
|||
const extraForm = new FormGroup({}); |
|||
form.addControl(EXTRA_PROPERTIES_KEY, extraForm); |
|||
|
|||
const record = data.record || {}; |
|||
const type = JSON.stringify(record) === '{}' ? 'create' : 'edit'; |
|||
const props: FormPropList<R> = extensions[`${type}FormProps`].get(identifier).props; |
|||
const extraProperties = record[EXTRA_PROPERTIES_KEY] || {}; |
|||
|
|||
props.forEach(({ value: prop }) => { |
|||
const name = prop.name; |
|||
const isExtraProperty = prop.isExtra || name in extraProperties; |
|||
let value = isExtraProperty ? extraProperties[name] : name in record ? record[name] : undefined; |
|||
|
|||
if (typeof value === 'undefined') value = prop.defaultValue; |
|||
|
|||
if (value) { |
|||
let adapter: DateAdapter | TimeAdapter | DateTimeAdapter; |
|||
switch (prop.type) { |
|||
case ePropType.Date: |
|||
adapter = new DateAdapter(); |
|||
value = adapter.toModel(adapter.fromModel(value)); |
|||
break; |
|||
case ePropType.Time: |
|||
adapter = new TimeAdapter(); |
|||
value = adapter.toModel(adapter.fromModel(value)); |
|||
break; |
|||
case ePropType.DateTime: |
|||
adapter = new DateTimeAdapter(); |
|||
value = adapter.toModel(adapter.fromModel(value) as any); |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
const formControl = new FormControl(value, { |
|||
asyncValidators: prop.asyncValidators(data), |
|||
validators: prop.validators(data), |
|||
}); |
|||
|
|||
(isExtraProperty ? extraForm : form).addControl(name, formControl); |
|||
}); |
|||
|
|||
return form; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
import { ApplicationConfiguration, createLocalizationPipeKeyGenerator } from '@abp/ng.core'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
|
|||
export function createDisplayNameLocalizationPipeKeyGenerator( |
|||
localization: ApplicationConfiguration.Localization, |
|||
) { |
|||
const generateLocalizationPipeKey = createLocalizationPipeKeyGenerator(localization); |
|||
|
|||
return (displayName: ObjectExtensions.DisplayName, fallback: ObjectExtensions.DisplayName) => { |
|||
if (displayName && displayName.name) |
|||
return generateLocalizationPipeKey( |
|||
[displayName.resource], |
|||
[displayName.name], |
|||
displayName.name, |
|||
); |
|||
|
|||
const key = generateLocalizationPipeKey( |
|||
[fallback.resource], |
|||
['DisplayName:' + fallback.name], |
|||
undefined, |
|||
); |
|||
|
|||
if (key) return key; |
|||
|
|||
return generateLocalizationPipeKey([fallback.resource], [fallback.name], fallback.name); |
|||
}; |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
import { of } from 'rxjs'; |
|||
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; |
|||
import { |
|||
EntityPropContributorCallbacks, |
|||
EntityPropDefaults, |
|||
EntityProps, |
|||
EntityPropsFactory, |
|||
} from '../models/entity-props'; |
|||
import { |
|||
CreateFormPropContributorCallbacks, |
|||
CreateFormPropDefaults, |
|||
CreateFormPropsFactory, |
|||
EditFormPropContributorCallbacks, |
|||
EditFormPropDefaults, |
|||
EditFormPropsFactory, |
|||
FormProps, |
|||
} from '../models/form-props'; |
|||
import { PropContributorCallback, PropData, PropList, PropsFactory } from '../models/props'; |
|||
|
|||
export function createExtraPropertyValueResolver<T>(name: string) { |
|||
return (data?: PropData<T>) => of(data.record[EXTRA_PROPERTIES_KEY][name]); |
|||
} |
|||
|
|||
export function mergeWithDefaultProps<F extends PropsFactory<any>>( |
|||
extension: F, |
|||
defaultProps: InferredPropDefaults<F>, |
|||
...contributors: InferredPropContributorCallbacks<F>[] |
|||
) { |
|||
Object.keys(defaultProps).forEach((name: string) => { |
|||
const props: InferredProps<F> = extension.get(name); |
|||
props.clearContributors(); |
|||
props.addContributor((propList: PropList) => propList.addManyTail(defaultProps[name])); |
|||
contributors.forEach(contributor => |
|||
(contributor[name] || []).forEach((callback: PropContributorCallback<any>) => |
|||
props.addContributor(callback), |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
type InferredPropDefaults<F> = F extends EntityPropsFactory<infer RE> |
|||
? EntityPropDefaults<RE> |
|||
: F extends CreateFormPropsFactory<infer RCF> |
|||
? CreateFormPropDefaults<RCF> |
|||
: F extends EditFormPropsFactory<infer REF> |
|||
? EditFormPropDefaults<REF> |
|||
: never; |
|||
|
|||
type InferredPropContributorCallbacks<F> = F extends EntityPropsFactory<infer RE> |
|||
? EntityPropContributorCallbacks<RE> |
|||
: F extends CreateFormPropsFactory<infer RCF> |
|||
? CreateFormPropContributorCallbacks<RCF> |
|||
: F extends EditFormPropsFactory<infer REF> |
|||
? EditFormPropContributorCallbacks<REF> |
|||
: never; |
|||
|
|||
type InferredProps<F> = F extends EntityPropsFactory<infer RE> |
|||
? EntityProps<RE> |
|||
: F extends CreateFormPropsFactory<infer RCF> |
|||
? FormProps<RCF> |
|||
: F extends EditFormPropsFactory<infer REF> |
|||
? FormProps<REF> |
|||
: never; |
|||
@ -0,0 +1,174 @@ |
|||
import { ABP, ApplicationConfiguration } from '@abp/ng.core'; |
|||
import { createSelector, Store } from '@ngxs/store'; |
|||
import { Observable, pipe, zip } from 'rxjs'; |
|||
import { filter, map, switchMap, take } from 'rxjs/operators'; |
|||
import { ePropType } from '../enums/props.enum'; |
|||
import { EntityProp, EntityPropList } from '../models/entity-props'; |
|||
import { FormProp, FormPropList } from '../models/form-props'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
import { PropCallback } from '../models/props'; |
|||
import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util'; |
|||
import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util'; |
|||
import { createExtraPropertyValueResolver } from './props.util'; |
|||
import { getValidatorsFromProperty } from './validation.util'; |
|||
|
|||
const selectConfig = (state: any) => state.ConfigState; |
|||
|
|||
const selectObjectExtensions = createSelector( |
|||
[selectConfig], |
|||
(config: ObjectExtensions.Config) => config.objectExtensions, |
|||
); |
|||
|
|||
const selectLocalization = createSelector( |
|||
[selectConfig], |
|||
(config: ApplicationConfiguration.Response) => config.localization, |
|||
); |
|||
|
|||
const selectEnums = createSelector( |
|||
[selectObjectExtensions, selectLocalization], |
|||
(extensions: ObjectExtensions.Item) => |
|||
Object.keys(extensions.enums).reduce((acc, key) => { |
|||
const { fields, localizationResource } = extensions.enums[key]; |
|||
acc[key] = { |
|||
fields, |
|||
localizationResource, |
|||
transformed: createEnum(fields), |
|||
}; |
|||
return acc; |
|||
}, {} as ObjectExtensions.Enums), |
|||
); |
|||
|
|||
const createObjectExtensionEntitiesSelector = (moduleKey: ModuleKey) => |
|||
createSelector([selectObjectExtensions], (extensions: ObjectExtensions.Item) => { |
|||
if (!extensions) return null; |
|||
|
|||
return (extensions.modules[moduleKey] || ({} as ObjectExtensions.Module)).entities; |
|||
}); |
|||
|
|||
export function getObjectExtensionEntitiesFromStore(store: Store, moduleKey: ModuleKey) { |
|||
return store.select(createObjectExtensionEntitiesSelector(moduleKey)).pipe( |
|||
map(entities => (isUndefined(entities) ? {} : entities)), |
|||
filter<ObjectExtensions.Entities>(Boolean), |
|||
take(1), |
|||
); |
|||
} |
|||
|
|||
export function mapEntitiesToContributors<T = any>(store: Store, resource: string) { |
|||
return pipe( |
|||
switchMap(entities => |
|||
zip(store.select(selectLocalization), store.select(selectEnums)).pipe( |
|||
map(([localization, enums]) => { |
|||
const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator(localization); |
|||
|
|||
return Object.keys(entities).reduce( |
|||
(acc, key: keyof ObjectExtensions.Entities) => { |
|||
acc.prop[key] = []; |
|||
acc.createForm[key] = []; |
|||
acc.editForm[key] = []; |
|||
|
|||
const entity: ObjectExtensions.Entity = entities[key]; |
|||
if (!entity) return acc; |
|||
|
|||
const properties = entity.properties; |
|||
if (!properties) return acc; |
|||
|
|||
const mapPropertiesToContributors = createPropertiesToContributorsMapper<T>( |
|||
generateDisplayName, |
|||
resource, |
|||
enums, |
|||
); |
|||
|
|||
return mapPropertiesToContributors(properties, acc, key); |
|||
}, |
|||
{ |
|||
prop: {}, |
|||
createForm: {}, |
|||
editForm: {}, |
|||
} as ObjectExtensions.PropContributors, |
|||
); |
|||
}), |
|||
), |
|||
), |
|||
take(1), |
|||
); |
|||
} |
|||
|
|||
function createPropertiesToContributorsMapper<T = any>( |
|||
generateDisplayName: DisplayNameGeneratorFn, |
|||
resource: string, |
|||
enums: Record<string, ObjectExtensions.Enum>, |
|||
) { |
|||
return ( |
|||
properties: Record<string, ObjectExtensions.Property>, |
|||
contributors: ObjectExtensions.PropContributors<T>, |
|||
key: string, |
|||
) => { |
|||
const isExtra = true; |
|||
|
|||
Object.keys(properties).forEach((name: string) => { |
|||
const property = properties[name]; |
|||
const type = getTypeFromProperty(property); |
|||
const displayName = generateDisplayName(property.displayName, { name, resource }); |
|||
|
|||
if (property.ui.onTable.isVisible) { |
|||
const sortable = Boolean(property.ui.onTable.isSortable); |
|||
const columnWidth = type === ePropType.Boolean ? 150 : 250; |
|||
const valueResolver = |
|||
type === ePropType.Enum |
|||
? createEnumValueResolver(property.type, enums[property.type], name) |
|||
: createExtraPropertyValueResolver<T>(name); |
|||
|
|||
const entityProp = new EntityProp<T>({ |
|||
type, |
|||
name, |
|||
displayName, |
|||
sortable, |
|||
columnWidth, |
|||
valueResolver, |
|||
isExtra, |
|||
}); |
|||
|
|||
const contributor = (propList: EntityPropList<T>) => propList.addTail(entityProp); |
|||
contributors.prop[key].push(contributor); |
|||
} |
|||
|
|||
const isOnCreateForm = property.ui.onCreateForm.isVisible; |
|||
const isOnEditForm = property.ui.onEditForm.isVisible; |
|||
|
|||
if (isOnCreateForm || isOnEditForm) { |
|||
const defaultValue = property.defaultValue; |
|||
const validators = () => getValidatorsFromProperty(property); |
|||
let options: PropCallback<any, Observable<ABP.Option<any>[]>>; |
|||
if (type === ePropType.Enum) options = createEnumOptions(name, enums[property.type]); |
|||
|
|||
const formProp = new FormProp({ |
|||
type, |
|||
name, |
|||
displayName, |
|||
options, |
|||
defaultValue, |
|||
validators, |
|||
isExtra, |
|||
}); |
|||
|
|||
const formContributor = (propList: FormPropList<T>) => propList.addTail(formProp); |
|||
|
|||
if (isOnCreateForm) contributors.createForm[key].push(formContributor); |
|||
if (isOnEditForm) contributors.editForm[key].push(formContributor); |
|||
} |
|||
}); |
|||
|
|||
return contributors; |
|||
}; |
|||
} |
|||
|
|||
function getTypeFromProperty(property: ObjectExtensions.Property): ePropType { |
|||
return (property.typeSimple.replace(/\?$/, '') as string) as ePropType; |
|||
} |
|||
|
|||
function isUndefined(obj: any): obj is undefined { |
|||
return typeof obj === 'undefined'; |
|||
} |
|||
|
|||
type DisplayNameGeneratorFn = ReturnType<typeof createDisplayNameLocalizationPipeKeyGenerator>; |
|||
type ModuleKey = keyof ObjectExtensions.Modules; |
|||
@ -0,0 +1,14 @@ |
|||
import { AbpValidators } from '@abp/ng.core'; |
|||
import { ValidatorFn } from '@angular/forms'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
|
|||
export function getValidatorsFromProperty(property: ObjectExtensions.Property): ValidatorFn[] { |
|||
const validators: ValidatorFn[] = []; |
|||
|
|||
property.attributes.forEach(attr => { |
|||
if (attr.typeSimple in AbpValidators) |
|||
validators.push(AbpValidators[attr.typeSimple](attr.config)); |
|||
}); |
|||
|
|||
return validators; |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
export * from './lib/adapters/date-time.adapter'; |
|||
export * from './lib/adapters/date.adapter'; |
|||
export * from './lib/adapters/time.adapter'; |
|||
export * from './lib/components/date-time-picker/date-time-picker.component'; |
|||
export * from './lib/components/extensible-form/extensible-form-prop.component'; |
|||
export * from './lib/components/extensible-form/extensible-form.component'; |
|||
export * from './lib/components/extensible-table/extensible-table.component'; |
|||
export * from './lib/components/grid-actions/grid-actions.component'; |
|||
export * from './lib/components/page-toolbar/page-toolbar.component'; |
|||
export * from './lib/constants/extra-properties'; |
|||
export * from './lib/directives/disabled.directive'; |
|||
export * from './lib/directives/prop-data.directive'; |
|||
export * from './lib/enums/props.enum'; |
|||
export { |
|||
ActionCallback, |
|||
ActionList, |
|||
ActionPredicate, |
|||
ReadonlyActionData as ActionData, |
|||
} from './lib/models/actions'; |
|||
export { |
|||
EntityAction, |
|||
EntityActionContributorCallback, |
|||
EntityActionList, |
|||
EntityActionOptions, |
|||
EntityActions, |
|||
EntityActionsFactory, |
|||
} from './lib/models/entity-actions'; |
|||
export { |
|||
EntityProp, |
|||
EntityPropContributorCallback, |
|||
EntityPropList, |
|||
EntityPropOptions, |
|||
EntityProps, |
|||
EntityPropsFactory, |
|||
} from './lib/models/entity-props'; |
|||
export { |
|||
CreateFormPropContributorCallback, |
|||
CreateFormPropsFactory, |
|||
EditFormPropContributorCallback, |
|||
EditFormPropsFactory, |
|||
FormProp, |
|||
FormPropData, |
|||
FormPropList, |
|||
FormPropOptions, |
|||
FormProps, |
|||
} from './lib/models/form-props'; |
|||
export * from './lib/models/object-extensions'; |
|||
export { |
|||
PropCallback, |
|||
PropList, |
|||
PropPredicate, |
|||
ReadonlyPropData as PropData, |
|||
} from './lib/models/props'; |
|||
export { |
|||
ToolbarAction, |
|||
ToolbarActionContributorCallback, |
|||
ToolbarActionList, |
|||
ToolbarActionOptions, |
|||
ToolbarActions, |
|||
ToolbarActionsFactory, |
|||
ToolbarComponent, |
|||
ToolbarComponentOptions, |
|||
} from './lib/models/toolbar-actions'; |
|||
export * from './lib/services/extensions.service'; |
|||
export * from './lib/tokens/extensions.token'; |
|||
export * from './lib/ui-extensions.module'; |
|||
export * from './lib/utils/actions.util'; |
|||
export * from './lib/utils/form-props.util'; |
|||
export * from './lib/utils/props.util'; |
|||
export * from './lib/utils/state.util'; |
|||
@ -0,0 +1,22 @@ |
|||
import { ActionData } from '../lib/models/actions'; |
|||
|
|||
describe('ActionData', () => { |
|||
describe('#data', () => { |
|||
it('should return record and getInjected', () => { |
|||
const spy = jest.fn(); |
|||
class Data extends ActionData<string> { |
|||
index = 0; |
|||
record = 'X'; |
|||
getInjected = spy; |
|||
} |
|||
|
|||
const data = new Data(); |
|||
data.data.getInjected(null); |
|||
|
|||
expect(spy).toHaveBeenCalledTimes(1); |
|||
expect(spy).toHaveBeenCalledWith(null); |
|||
expect(data.data.index).toBe(0); |
|||
expect(data.data.record).toBe('X'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,53 @@ |
|||
import { |
|||
EntityAction, |
|||
EntityActionContributorCallbacks, |
|||
EntityActionDefaults, |
|||
EntityActionsFactory, |
|||
} from '../lib/models/entity-actions'; |
|||
import { mergeWithDefaultActions } from '../lib/utils/actions.util'; |
|||
|
|||
describe('Entity Action Utils', () => { |
|||
describe('#mergeEntityActions', () => { |
|||
let entityActions: EntityActionsFactory; |
|||
|
|||
beforeEach(() => { |
|||
entityActions = new EntityActionsFactory(); |
|||
}); |
|||
|
|||
it('should merge default actions with action contributors', () => { |
|||
const defaults: EntityActionDefaults = { |
|||
x: [(1 as any) as EntityAction, (2 as any) as EntityAction, (3 as any) as EntityAction], |
|||
y: [(1 as any) as EntityAction, (2 as any) as EntityAction, (3 as any) as EntityAction], |
|||
}; |
|||
|
|||
const contributors1: EntityActionContributorCallbacks = { |
|||
x: [ |
|||
actionList => { |
|||
const x2 = actionList.dropByIndex(1); // 1 <-> 3
|
|||
actionList.addHead(x2.value); // 2 <-> 1 <-> 3
|
|||
}, |
|||
actionList => { |
|||
actionList.dropTail(); // 2 <-> 1
|
|||
}, |
|||
], |
|||
}; |
|||
|
|||
const contributors2: EntityActionContributorCallbacks = { |
|||
y: [ |
|||
actionList => { |
|||
const y2 = actionList.dropByIndex(1); // 1 <-> 3
|
|||
actionList.addTail(y2.value); // 1 <-> 3 <-> 2
|
|||
}, |
|||
actionList => { |
|||
actionList.dropHead(); // 3 <-> 2
|
|||
}, |
|||
], |
|||
}; |
|||
|
|||
mergeWithDefaultActions(entityActions, defaults, contributors1, contributors2); |
|||
|
|||
expect(entityActions.get('x').actions.toString()).toBe('2 <-> 1'); |
|||
expect(entityActions.get('y').actions.toString()).toBe('3 <-> 2'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,35 @@ |
|||
import { DateTimeAdapter } from '../lib/adapters/date-time.adapter'; |
|||
|
|||
describe('DateTime Adapter', () => { |
|||
const adapter = new DateTimeAdapter(); |
|||
const date = new Date(2002, 2, 30, 13, 30, 45, 0); |
|||
const year = date.getFullYear(); |
|||
const month = date.getMonth() + 1; |
|||
const day = date.getDate(); |
|||
const hour = date.getHours(); |
|||
const minute = date.getMinutes(); |
|||
const second = date.getSeconds(); |
|||
|
|||
describe('#fromModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${null} |
|||
${null} | ${null} |
|||
${'x'} | ${null} |
|||
${date} | ${{ year, month, day, hour, minute, second }} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.fromModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
|
|||
describe('#toModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${''} |
|||
${null} | ${''} |
|||
${{ year, month, day, hour, minute, second }} | ${date.toISOString()} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.toModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,31 @@ |
|||
import { DateAdapter } from '../lib/adapters/date.adapter'; |
|||
|
|||
describe('Date Adapter', () => { |
|||
const adapter = new DateAdapter(); |
|||
|
|||
describe('#fromModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${null} |
|||
${null} | ${null} |
|||
${'x'} | ${null} |
|||
${'2002-03-30'} | ${{ day: 30, month: 3, year: 2002 }} |
|||
${'03/30/2002'} | ${{ day: 30, month: 3, year: 2002 }} |
|||
${new Date(0)} | ${{ day: 1, month: 1, year: 1970 }} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.fromModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
|
|||
describe('#toModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${''} |
|||
${null} | ${''} |
|||
${{ day: 30, month: 3, year: 2002 }} | ${'2002-03-30'} |
|||
${{ day: 1, month: 1, year: 1970 }} | ${'1970-01-01'} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.toModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,151 @@ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { |
|||
EntityAction, |
|||
EntityActionContributorCallback, |
|||
EntityActionList, |
|||
EntityActions, |
|||
EntityActionsFactory, |
|||
} from '../lib/models/entity-actions'; |
|||
|
|||
describe('EntityActionList', () => { |
|||
it('should inherit from LinkedList', () => { |
|||
expect(new EntityActionList() instanceof LinkedList).toBe(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityActions', () => { |
|||
const add1toTail: EntityActionContributorCallback = actionList => actionList.addTail(1 as any); |
|||
const add2toTail: EntityActionContributorCallback = actionList => actionList.addTail(2 as any); |
|||
const add3toTail: EntityActionContributorCallback = actionList => actionList.addTail(3 as any); |
|||
const dropIndex1: EntityActionContributorCallback = actionList => actionList.dropByIndex(1); |
|||
|
|||
describe('#actions', () => { |
|||
test.each` |
|||
callbackList | expected |
|||
${[]} | ${''} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} |
|||
${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} |
|||
${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} |
|||
`(
|
|||
'should return $expected when given callbackList is $callbackList', |
|||
({ callbackList, expected }) => { |
|||
const creator = new EntityActions(callbackList); |
|||
expect(creator.actions.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#addContributor', () => { |
|||
const creator = new EntityActions([]); |
|||
|
|||
test.each` |
|||
callbackList | callback | expected |
|||
${[]} | ${add1toTail} | ${'1'} |
|||
${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} |
|||
${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} |
|||
`(
|
|||
'should set actions to $expected when callbackList is $callbackList and given callback is $callback', |
|||
({ callback, expected }) => { |
|||
creator.addContributor(callback); |
|||
expect(creator.actions.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityActionsFactory', () => { |
|||
describe('#get', () => { |
|||
it('should create and return an EntityActions instance', () => { |
|||
const entityActions = new EntityActionsFactory(); |
|||
const creator = entityActions.get(''); |
|||
|
|||
expect(creator).toBeInstanceOf(EntityActions); |
|||
}); |
|||
|
|||
it('should store and pass contributorCallbacks to EntityActionsCreator instance it returns', () => { |
|||
const entityActions = new EntityActionsFactory(); |
|||
const creatorX1 = entityActions.get('X'); |
|||
|
|||
expect(creatorX1).toBeInstanceOf(EntityActions); |
|||
expect(creatorX1.actions.toArray()).toHaveLength(0); |
|||
|
|||
creatorX1.addContributor(actionList => actionList.addTail(1 as any)); |
|||
|
|||
const creatorX2 = entityActions.get('X'); |
|||
expect(creatorX2.actions.toArray()).toHaveLength(1); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityAction', () => { |
|||
it('should be created when options object is passed as argument', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
permission: 'PERMISSION', |
|||
visible: () => false, |
|||
icon: 'ICON', |
|||
}; |
|||
|
|||
const action = new EntityAction(options); |
|||
|
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action(null)).toBe(options.action()); |
|||
expect(action.permission).toBe(options.permission); |
|||
expect(action.visible(null)).toBe(options.visible()); |
|||
expect(action.icon).toBe(options.icon); |
|||
}); |
|||
|
|||
it('should be created when only required options are passed', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
}; |
|||
|
|||
const action = new EntityAction(options); |
|||
|
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action).toBe(options.action); |
|||
expect(action.permission).toBeUndefined(); |
|||
expect(action.visible(null)).toBe(true); |
|||
expect(action.icon).toBe(''); |
|||
}); |
|||
|
|||
describe('#create', () => { |
|||
it('should return a new instance from given options', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
}; |
|||
|
|||
const action = EntityAction.create(options); |
|||
|
|||
expect(action).toBeInstanceOf(EntityAction); |
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action).toBe(options.action); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createMany', () => { |
|||
it('should return multiple instances from given options array', () => { |
|||
const options1 = { |
|||
text: 'TEXT 1', |
|||
action: () => 'ACTION 1', |
|||
}; |
|||
const options2 = { |
|||
text: 'TEXT 2', |
|||
action: () => 'ACTION 2', |
|||
}; |
|||
|
|||
const [action1, action2] = EntityAction.createMany([options1, options2]); |
|||
|
|||
expect(action1).toBeInstanceOf(EntityAction); |
|||
expect(action1.text).toBe(options1.text); |
|||
expect(action1.action).toBe(options1.action); |
|||
expect(action2).toBeInstanceOf(EntityAction); |
|||
expect(action2.text).toBe(options2.text); |
|||
expect(action2.action).toBe(options2.action); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,166 @@ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { NEVER } from 'rxjs'; |
|||
import { ePropType } from '../lib/enums/props.enum'; |
|||
import { |
|||
EntityProp, |
|||
EntityPropContributorCallback, |
|||
EntityPropList, |
|||
EntityProps, |
|||
EntityPropsFactory, |
|||
} from '../lib/models/entity-props'; |
|||
import { PropData } from '../lib/models/props'; |
|||
|
|||
describe('EntityPropList', () => { |
|||
it('should inherit from LinkedList', () => { |
|||
expect(new EntityPropList() instanceof LinkedList).toBe(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityProps', () => { |
|||
const add1toTail: EntityPropContributorCallback = propList => propList.addTail(1 as any); |
|||
const add2toTail: EntityPropContributorCallback = propList => propList.addTail(2 as any); |
|||
const add3toTail: EntityPropContributorCallback = propList => propList.addTail(3 as any); |
|||
const dropIndex1: EntityPropContributorCallback = propList => propList.dropByIndex(1); |
|||
|
|||
describe('#props', () => { |
|||
test.each` |
|||
callbackList | expected |
|||
${[]} | ${''} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} |
|||
${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} |
|||
${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} |
|||
`(
|
|||
'should return $expected when given callbackList is $callbackList', |
|||
({ callbackList, expected }) => { |
|||
const creator = new EntityProps(callbackList); |
|||
expect(creator.props.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#addContributor', () => { |
|||
const creator = new EntityProps([]); |
|||
|
|||
test.each` |
|||
callbackList | callback | expected |
|||
${[]} | ${add1toTail} | ${'1'} |
|||
${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} |
|||
${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} |
|||
`(
|
|||
'should set props to $expected when callbackList is $callbackList and given callback is $callback', |
|||
({ callback, expected }) => { |
|||
creator.addContributor(callback); |
|||
expect(creator.props.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityPropsFactory', () => { |
|||
describe('#get', () => { |
|||
it('should create and return an EntityProps instance', () => { |
|||
const entityProps = new EntityPropsFactory(); |
|||
const creator = entityProps.get(''); |
|||
|
|||
expect(creator).toBeInstanceOf(EntityProps); |
|||
}); |
|||
|
|||
it('should store and pass contributorCallbacks to EntityPropsCreator instance it returns', () => { |
|||
const entityProps = new EntityPropsFactory(); |
|||
const creatorX1 = entityProps.get('X'); |
|||
|
|||
expect(creatorX1).toBeInstanceOf(EntityProps); |
|||
expect(creatorX1.props.toArray()).toHaveLength(0); |
|||
|
|||
creatorX1.addContributor(propList => propList.addTail(1 as any)); |
|||
|
|||
const creatorX2 = entityProps.get('X'); |
|||
expect(creatorX2.props.toArray()).toHaveLength(1); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityProp', () => { |
|||
it('should be created when options object is passed as argument', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
displayName: 'DISPLAY NAME', |
|||
permission: 'PERMISSION', |
|||
visible: () => false, |
|||
valueResolver: () => NEVER, |
|||
sortable: true, |
|||
columnWidth: 999, |
|||
}; |
|||
|
|||
const prop = new EntityProp(options); |
|||
|
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
expect(prop.displayName).toBe(options.displayName); |
|||
expect(prop.permission).toBe(options.permission); |
|||
expect(prop.visible()).toBe(options.visible()); |
|||
expect(prop.valueResolver()).toBe(options.valueResolver()); |
|||
expect(prop.sortable).toBe(options.sortable); |
|||
expect(prop.columnWidth).toBe(options.columnWidth); |
|||
}); |
|||
|
|||
it('should be created when only required options are passed', done => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
}; |
|||
|
|||
const prop = new EntityProp(options); |
|||
|
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
expect(prop.displayName).toBe(options.name); |
|||
expect(prop.permission).toBeUndefined(); |
|||
expect(prop.visible()).toBe(true); |
|||
expect(prop.sortable).toBe(false); |
|||
expect(prop.columnWidth).toBeUndefined(); |
|||
prop.valueResolver({ record: { NAME: 'X' } } as PropData).subscribe(value => { |
|||
expect(value).toBe('X'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
describe('#create', () => { |
|||
it('should return a new instance from given options', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
}; |
|||
|
|||
const prop = EntityProp.create(options); |
|||
|
|||
expect(prop).toBeInstanceOf(EntityProp); |
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createMany', () => { |
|||
it('should return multiple instances from given options array', () => { |
|||
const options1 = { |
|||
type: ePropType.String, |
|||
name: 'NAME 1', |
|||
}; |
|||
const options2 = { |
|||
type: ePropType.Boolean, |
|||
name: 'NAME 2', |
|||
}; |
|||
|
|||
const [prop1, prop2] = EntityProp.createMany([options1, options2]); |
|||
|
|||
expect(prop1).toBeInstanceOf(EntityProp); |
|||
expect(prop1.type).toBe(options1.type); |
|||
expect(prop1.name).toBe(options1.name); |
|||
expect(prop2).toBeInstanceOf(EntityProp); |
|||
expect(prop2.type).toBe(options2.type); |
|||
expect(prop2.name).toBe(options2.name); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,133 @@ |
|||
import { LocalizationService } from '@abp/ng.core'; |
|||
import { Store } from '@ngxs/store'; |
|||
import { Subject } from 'rxjs'; |
|||
import { take } from 'rxjs/operators'; |
|||
import { PropData } from '../lib/models/props'; |
|||
import { createEnum, createEnumOptions, createEnumValueResolver } from '../lib/utils/enum.util'; |
|||
|
|||
const fields = [ |
|||
{ name: 'foo', value: 1 }, |
|||
{ name: 'bar', value: 2 }, |
|||
{ name: 'baz', value: 3 }, |
|||
]; |
|||
|
|||
class MockPropData<R = any> extends PropData<R> { |
|||
getInjected: PropData<R>['getInjected']; |
|||
|
|||
constructor(public readonly record: R) { |
|||
super(); |
|||
} |
|||
} |
|||
|
|||
describe('Enum Utils', () => { |
|||
describe('#createEnum', () => { |
|||
const enumFromFields = createEnum(fields); |
|||
|
|||
test.each` |
|||
key | expected |
|||
${'foo'} | ${1} |
|||
${'bar'} | ${2} |
|||
${'baz'} | ${3} |
|||
${1} | ${'foo'} |
|||
${2} | ${'bar'} |
|||
${3} | ${'baz'} |
|||
`('should create an enum that returns $expected when $key is accessed', ({ key, expected }) => {
|
|||
expect(enumFromFields[key]).toBe(expected); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createEnumValueResolver', () => { |
|||
const service = new LocalizationService( |
|||
new Subject().asObservable(), |
|||
({ |
|||
selectSnapshot: () => ({ |
|||
values: { |
|||
Default: { |
|||
'Enum:MyEnum.foo': 'Foo', |
|||
'MyEnum.bar': 'Bar', |
|||
baz: 'Baz', |
|||
}, |
|||
}, |
|||
defaultResourceName: 'Default', |
|||
currentCulture: null, |
|||
languages: [], |
|||
}), |
|||
} as unknown) as Store, |
|||
null, |
|||
null, |
|||
null, |
|||
); |
|||
|
|||
test.each` |
|||
value | expected |
|||
${1} | ${'Foo'} |
|||
${2} | ${'Bar'} |
|||
${3} | ${'Baz'} |
|||
`(
|
|||
'should create a resolver that returns observable $expected when enum value is $value', |
|||
async ({ value, expected }) => { |
|||
const valueResolver = createEnumValueResolver( |
|||
'MyCompanyName.MyProjectName.MyEnum', |
|||
{ |
|||
fields, |
|||
localizationResource: null, |
|||
transformed: createEnum(fields), |
|||
}, |
|||
'EnumProp', |
|||
); |
|||
const propData = new MockPropData({ extraProperties: { EnumProp: value } }); |
|||
propData.getInjected = () => service as any; |
|||
|
|||
const resolved = await valueResolver(propData) |
|||
.pipe(take(1)) |
|||
.toPromise(); |
|||
|
|||
expect(resolved).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#createEnumOptions', () => { |
|||
const service = new LocalizationService( |
|||
new Subject().asObservable(), |
|||
({ |
|||
selectSnapshot: () => ({ |
|||
values: { |
|||
Default: { |
|||
'Enum:MyEnum.foo': 'Foo', |
|||
'MyEnum.bar': 'Bar', |
|||
baz: 'Baz', |
|||
}, |
|||
}, |
|||
defaultResourceName: 'Default', |
|||
currentCulture: null, |
|||
languages: [], |
|||
}), |
|||
} as unknown) as Store, |
|||
null, |
|||
null, |
|||
null, |
|||
); |
|||
|
|||
it('should create a generator that returns observable options from enums', async () => { |
|||
const options = createEnumOptions('MyCompanyName.MyProjectName.MyEnum', { |
|||
fields, |
|||
localizationResource: null, |
|||
transformed: createEnum(fields), |
|||
}); |
|||
|
|||
const propData = new MockPropData({}); |
|||
propData.getInjected = () => service as any; |
|||
|
|||
const resolved = await options(propData) |
|||
.pipe(take(1)) |
|||
.toPromise(); |
|||
|
|||
expect(resolved).toEqual([ |
|||
{ key: 'Foo', value: 1 }, |
|||
{ key: 'Bar', value: 2 }, |
|||
{ key: 'Baz', value: 3 }, |
|||
]); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,50 @@ |
|||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; |
|||
import { EntityActionsFactory } from '../lib/models/entity-actions'; |
|||
import { EntityPropsFactory } from '../lib/models/entity-props'; |
|||
import { CreateFormPropsFactory, EditFormPropsFactory } from '../lib/models/form-props'; |
|||
import { ToolbarActionsFactory } from '../lib/models/toolbar-actions'; |
|||
import { ExtensionsService } from '../lib/services/extensions.service'; |
|||
|
|||
describe('ExtensionsService', () => { |
|||
let service: ExtensionsService; |
|||
let spectator: SpectatorService<ExtensionsService>; |
|||
|
|||
const createService = createServiceFactory({ |
|||
service: ExtensionsService, |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createService(); |
|||
service = spectator.service; |
|||
}); |
|||
|
|||
describe('#entityActions', () => { |
|||
it('should be an instance of EntityActionsFactory class', () => { |
|||
expect(service.entityActions).toBeInstanceOf(EntityActionsFactory); |
|||
}); |
|||
}); |
|||
|
|||
describe('#toolbarActions', () => { |
|||
it('should be an instance of ToolbarActionsFactory class', () => { |
|||
expect(service.toolbarActions).toBeInstanceOf(ToolbarActionsFactory); |
|||
}); |
|||
}); |
|||
|
|||
describe('#entityProps', () => { |
|||
it('should be an instance of EntityPropsFactory class', () => { |
|||
expect(service.entityProps).toBeInstanceOf(EntityPropsFactory); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createFormProps', () => { |
|||
it('should be an instance of CreateFormPropsFactory class', () => { |
|||
expect(service.createFormProps).toBeInstanceOf(CreateFormPropsFactory); |
|||
}); |
|||
}); |
|||
|
|||
describe('#editFormProps', () => { |
|||
it('should be an instance of EditFormPropsFactory class', () => { |
|||
expect(service.editFormProps).toBeInstanceOf(EditFormPropsFactory); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,28 @@ |
|||
import { selfFactory } from '../lib/utils/factory.util'; |
|||
|
|||
describe('Factory Utils', () => { |
|||
describe('#selfFactory', () => { |
|||
const arr = []; |
|||
const obj = {}; |
|||
const date = new Date(); |
|||
const promise = Promise.resolve(null); |
|||
|
|||
test.each` |
|||
parameter |
|||
${'x'} |
|||
${''} |
|||
${1} |
|||
${0} |
|||
${true} |
|||
${false} |
|||
${arr} |
|||
${obj} |
|||
${date} |
|||
${promise} |
|||
${null} |
|||
${undefined} |
|||
`('should return given $parameter back', ({ parameter }) => {
|
|||
expect(selfFactory(parameter)).toBe(parameter); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,193 @@ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { NEVER } from 'rxjs'; |
|||
import { ePropType } from '../lib/enums/props.enum'; |
|||
import { |
|||
CreateFormPropContributorCallback, |
|||
CreateFormPropsFactory, |
|||
EditFormPropsFactory, |
|||
FormProp, |
|||
FormPropList, |
|||
FormProps, |
|||
} from '../lib/models/form-props'; |
|||
|
|||
describe('FormPropList', () => { |
|||
it('should inherit from LinkedList', () => { |
|||
expect(new FormPropList() instanceof LinkedList).toBe(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('FormProps', () => { |
|||
const add1toTail: CreateFormPropContributorCallback = propList => propList.addTail(1 as any); |
|||
const add2toTail: CreateFormPropContributorCallback = propList => propList.addTail(2 as any); |
|||
const add3toTail: CreateFormPropContributorCallback = propList => propList.addTail(3 as any); |
|||
const dropIndex1: CreateFormPropContributorCallback = propList => propList.dropByIndex(1); |
|||
|
|||
describe('#props', () => { |
|||
test.each` |
|||
callbackList | expected |
|||
${[]} | ${''} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} |
|||
${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} |
|||
${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} |
|||
`(
|
|||
'should return $expected when given callbackList is $callbackList', |
|||
({ callbackList, expected }) => { |
|||
const creator = new FormProps(callbackList); |
|||
expect(creator.props.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#addContributor', () => { |
|||
const creator = new FormProps([]); |
|||
|
|||
test.each` |
|||
callbackList | callback | expected |
|||
${[]} | ${add1toTail} | ${'1'} |
|||
${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} |
|||
${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} |
|||
`(
|
|||
'should set props to $expected when callbackList is $callbackList and given callback is $callback', |
|||
({ callback, expected }) => { |
|||
creator.addContributor(callback); |
|||
expect(creator.props.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe('FormPropsFactory', () => { |
|||
describe('#get', () => { |
|||
it('should create and return an FormProps instance', () => { |
|||
const formProps = new CreateFormPropsFactory(); |
|||
const creator = formProps.get(''); |
|||
|
|||
expect(creator).toBeInstanceOf(FormProps); |
|||
}); |
|||
|
|||
it('should store and pass contributorCallbacks to FormPropsCreator instance it returns', () => { |
|||
const formProps = new EditFormPropsFactory(); |
|||
const creatorX1 = formProps.get('X'); |
|||
|
|||
expect(creatorX1).toBeInstanceOf(FormProps); |
|||
expect(creatorX1.props.toArray()).toHaveLength(0); |
|||
|
|||
creatorX1.addContributor(propList => propList.addTail(1 as any)); |
|||
|
|||
const creatorX2 = formProps.get('X'); |
|||
expect(creatorX2.props.toArray()).toHaveLength(1); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('FormProp', () => { |
|||
it('should be created when options object is passed as argument', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
displayName: 'DISPLAY NAME', |
|||
permission: 'PERMISSION', |
|||
visible: () => false, |
|||
asyncValidators: () => [null], |
|||
validators: () => [null], |
|||
disabled: () => true, |
|||
readonly: () => true, |
|||
autocomplete: 'AUTOCOMPLETE', |
|||
defaultValue: 'DEFAULT VALUE', |
|||
options: () => NEVER, |
|||
id: 'ID', |
|||
}; |
|||
|
|||
const prop = new FormProp(options); |
|||
|
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
expect(prop.displayName).toBe(options.displayName); |
|||
expect(prop.permission).toBe(options.permission); |
|||
expect(prop.visible()).toBe(options.visible()); |
|||
expect(prop.asyncValidators()).toEqual(options.asyncValidators()); |
|||
expect(prop.validators()).toEqual(options.validators()); |
|||
expect(prop.disabled()).toBe(options.disabled()); |
|||
expect(prop.readonly()).toBe(options.readonly()); |
|||
expect(prop.autocomplete).toBe(options.autocomplete); |
|||
expect(prop.defaultValue).toBe(options.defaultValue); |
|||
expect(prop.options()).toBe(options.options()); |
|||
expect(prop.id).toBe(options.id); |
|||
}); |
|||
|
|||
it('should be created when only required options are passed', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
}; |
|||
|
|||
const prop = new FormProp(options); |
|||
|
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
expect(prop.displayName).toBe(options.name); |
|||
expect(prop.permission).toBeUndefined(); |
|||
expect(prop.visible()).toBe(true); |
|||
expect(prop.asyncValidators()).toEqual([]); |
|||
expect(prop.validators()).toEqual([]); |
|||
expect(prop.disabled()).toBe(false); |
|||
expect(prop.readonly()).toBe(false); |
|||
expect(prop.autocomplete).toBe('off'); |
|||
expect(prop.defaultValue).toBeNull(); |
|||
expect(prop.options).toBeUndefined(); |
|||
expect(prop.id).toBe(options.name); |
|||
}); |
|||
|
|||
test.each` |
|||
defaultValue | expected |
|||
${0} | ${0} |
|||
${''} | ${''} |
|||
${false} | ${false} |
|||
${undefined} | ${null} |
|||
`(
|
|||
'should set defaultValue as $expected when $defaultValue is given', |
|||
({ defaultValue, expected }) => { |
|||
const options = { type: null, name: null, defaultValue }; |
|||
const prop = new FormProp(options); |
|||
expect(prop.defaultValue).toBe(expected); |
|||
}, |
|||
); |
|||
|
|||
describe('#create', () => { |
|||
it('should return a new instance from given options', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
}; |
|||
|
|||
const prop = FormProp.create(options); |
|||
|
|||
expect(prop).toBeInstanceOf(FormProp); |
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createMany', () => { |
|||
it('should return multiple instances from given options array', () => { |
|||
const options1 = { |
|||
type: ePropType.String, |
|||
name: 'NAME 1', |
|||
}; |
|||
const options2 = { |
|||
type: ePropType.Boolean, |
|||
name: 'NAME 2', |
|||
}; |
|||
|
|||
const [prop1, prop2] = FormProp.createMany([options1, options2]); |
|||
|
|||
expect(prop1).toBeInstanceOf(FormProp); |
|||
expect(prop1.type).toBe(options1.type); |
|||
expect(prop1.name).toBe(options1.name); |
|||
expect(prop2).toBeInstanceOf(FormProp); |
|||
expect(prop2.type).toBe(options2.type); |
|||
expect(prop2.name).toBe(options2.name); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,142 @@ |
|||
import { LocalizationService } from '@abp/ng.core'; |
|||
import { Injector } from '@angular/core'; |
|||
import { FormControl, FormGroup, Validators } from '@angular/forms'; |
|||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; |
|||
import { ePropType } from '../lib/enums/props.enum'; |
|||
import { FormProp, FormPropData } from '../lib/models/form-props'; |
|||
import { ExtensionsService } from '../lib/services/extensions.service'; |
|||
import { EXTENSIONS_IDENTIFIER } from '../lib/tokens/extensions.token'; |
|||
import { generateFormFromProps } from '../lib/utils/form-props.util'; |
|||
|
|||
describe('Form Prop Utils', () => { |
|||
describe('#generateFormFromProps', () => { |
|||
let spectator: SpectatorService<ExtensionsService<Foo>>; |
|||
let injector: Injector; |
|||
const identifier = 'X'; |
|||
|
|||
const createService = createServiceFactory({ |
|||
service: ExtensionsService, |
|||
providers: [ |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: identifier, |
|||
}, |
|||
{ |
|||
provide: LocalizationService, |
|||
useValue: { currentLang: 'en' }, |
|||
}, |
|||
], |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createService(); |
|||
const props = FormProp.createMany<Foo>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'foo', |
|||
validators: () => [Validators.required], |
|||
defaultValue: 'bar', |
|||
}, |
|||
{ |
|||
type: ePropType.Boolean, |
|||
name: 'bool', |
|||
}, |
|||
{ |
|||
type: ePropType.Date, |
|||
name: 'date', |
|||
}, |
|||
{ |
|||
type: ePropType.DateTime, |
|||
name: 'dateTime', |
|||
}, |
|||
{ |
|||
type: ePropType.Time, |
|||
name: 'time', |
|||
}, |
|||
]); |
|||
|
|||
spectator.service.createFormProps |
|||
.get(identifier) |
|||
.addContributor(propList => propList.addManyTail(props)); |
|||
spectator.service.editFormProps |
|||
.get(identifier) |
|||
.addContributor(propList => propList.addManyTail(props)); |
|||
|
|||
const generator = getInjected(spectator); |
|||
injector = { |
|||
get: () => generator.next().value as any, |
|||
}; |
|||
}); |
|||
|
|||
it('should return a blank FormGroup instance', () => { |
|||
const data = new FormPropData<Foo>(injector, null); |
|||
|
|||
const formGroup = generateFormFromProps(data); |
|||
expect(formGroup).toBeInstanceOf(FormGroup); |
|||
expect(formGroup.value.foo).toBe('bar'); |
|||
|
|||
const formControl = formGroup.get('foo'); |
|||
expect(formControl).toBeInstanceOf(FormControl); |
|||
expect(formControl.valid).toBe(true); |
|||
}); |
|||
|
|||
it('should return a prefilled FormGroup instance', () => { |
|||
const data = new FormPropData<Foo>(injector, { id: 1, foo: null }); |
|||
|
|||
const formGroup = generateFormFromProps(data); |
|||
expect(formGroup).toBeInstanceOf(FormGroup); |
|||
expect(formGroup.value.foo).toBe(null); |
|||
|
|||
const formControl = formGroup.get('foo'); |
|||
expect(formControl).toBeInstanceOf(FormControl); |
|||
expect(formControl.invalid).toBe(true); |
|||
}); |
|||
|
|||
it('should add a FormGroup named extraProperties', () => { |
|||
const data = new FormPropData<Foo>(injector, null); |
|||
|
|||
const formGroup = generateFormFromProps(data); |
|||
const extraPropertiesGroup = formGroup.get('extraProperties'); |
|||
expect(extraPropertiesGroup).toBeInstanceOf(FormGroup); |
|||
}); |
|||
|
|||
it('should add extraProperties to extraProperties FormGroup', () => { |
|||
const data = new FormPropData<Foo>(injector, { |
|||
id: 1, |
|||
foo: undefined, |
|||
extraProperties: { |
|||
bool: true, |
|||
date: '03/30/2002', |
|||
dateTime: '2002-03-30 13:30:59Z', |
|||
time: '13:30:59', |
|||
}, |
|||
}); |
|||
|
|||
const formGroup = generateFormFromProps(data); |
|||
const extraPropertiesGroup = formGroup.get('extraProperties'); |
|||
expect(extraPropertiesGroup.value).toEqual({ |
|||
bool: true, |
|||
date: '2002-03-30', |
|||
dateTime: '2002-03-30T13:30:59.000Z', |
|||
time: '13:30', |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
function* getInjected(spectator: SpectatorService<ExtensionsService>) { |
|||
yield spectator.service; |
|||
yield spectator.get(EXTENSIONS_IDENTIFIER); |
|||
yield spectator.get(LocalizationService); |
|||
} |
|||
|
|||
interface Foo { |
|||
id: number; |
|||
foo: string; |
|||
extraProperties?: { |
|||
bool: boolean; |
|||
date: string; |
|||
dateTime: string; |
|||
time: string; |
|||
}; |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
import { createDisplayNameLocalizationPipeKeyGenerator } from '../lib/utils/localization.util'; |
|||
|
|||
describe('Localization Utils', () => { |
|||
describe('#createDisplayNameLocalizationPipeKeyGenerator', () => { |
|||
const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator({ |
|||
values: { |
|||
Foo: { Bar: 'Bar', 'DisplayName:Bar': 'Bar' }, |
|||
Default: { Bar: 'Bar', 'DisplayName:Bar': 'Bar' }, |
|||
}, |
|||
defaultResourceName: 'Default', |
|||
currentCulture: null, |
|||
languages: [], |
|||
}); |
|||
|
|||
test.each` |
|||
displayName | fallback | expected |
|||
${{ name: 'Bar', resource: 'Foo' }} | ${null} | ${'Foo::Bar'} |
|||
${{ name: 'Baz', resource: 'Foo' }} | ${null} | ${'Baz'} |
|||
${null} | ${{ name: 'Bar', resource: 'Foo' }} | ${'Foo::DisplayName:Bar'} |
|||
${null} | ${{ name: 'Bar', resource: 'Default' }} | ${'Default::DisplayName:Bar'} |
|||
${null} | ${{ name: 'Baz', resource: 'Default' }} | ${'Baz'} |
|||
`(
|
|||
'should return $expected when diplay name is $displayName and fallback is $fallback', |
|||
({ displayName, fallback, expected }) => { |
|||
const result = generateDisplayName(displayName, fallback); |
|||
|
|||
expect(result).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,22 @@ |
|||
import { PropData } from '../lib/models/props'; |
|||
|
|||
describe('PropData', () => { |
|||
describe('#data', () => { |
|||
it('should return record and getInjected', () => { |
|||
const spy = jest.fn(); |
|||
class Data extends PropData<string> { |
|||
index = 0; |
|||
record = 'X'; |
|||
getInjected = spy; |
|||
} |
|||
|
|||
const data = new Data(); |
|||
data.data.getInjected(null); |
|||
|
|||
expect(spy).toHaveBeenCalledTimes(1); |
|||
expect(spy).toHaveBeenCalledWith(null); |
|||
expect(data.data.index).toBe(0); |
|||
expect(data.data.record).toBe('X'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,72 @@ |
|||
import { |
|||
EntityProp, |
|||
EntityPropContributorCallbacks, |
|||
EntityPropDefaults, |
|||
EntityPropsFactory, |
|||
} from '../lib/models/entity-props'; |
|||
import { PropData } from '../lib/models/props'; |
|||
import { createExtraPropertyValueResolver, mergeWithDefaultProps } from '../lib/utils/props.util'; |
|||
|
|||
class MockPropData<R = any> extends PropData<R> { |
|||
getInjected: PropData<R>['getInjected']; |
|||
|
|||
constructor(public readonly record: R) { |
|||
super(); |
|||
} |
|||
} |
|||
|
|||
describe('Entity Prop Utils', () => { |
|||
describe('#createExtraPropertyValueResolver', () => { |
|||
it('should return a resolver that resolves an observable value from extraProperties', async () => { |
|||
const valueResolver = createExtraPropertyValueResolver('foo'); |
|||
const propData = new MockPropData({ extraProperties: { foo: 'bar' } }); |
|||
|
|||
const bar = await valueResolver(propData).toPromise(); |
|||
expect(bar).toBe('bar'); |
|||
}); |
|||
}); |
|||
|
|||
describe('#mergeEntityProps', () => { |
|||
let entityProps: EntityPropsFactory; |
|||
|
|||
beforeEach(() => { |
|||
entityProps = new EntityPropsFactory(); |
|||
}); |
|||
|
|||
it('should merge default props with prop contributors', () => { |
|||
const defaults: EntityPropDefaults = { |
|||
x: [(1 as any) as EntityProp, (2 as any) as EntityProp, (3 as any) as EntityProp], |
|||
y: [(1 as any) as EntityProp, (2 as any) as EntityProp, (3 as any) as EntityProp], |
|||
}; |
|||
|
|||
const contributors1: EntityPropContributorCallbacks = { |
|||
x: [ |
|||
propList => { |
|||
const x2 = propList.dropByIndex(1); // 1 <-> 3
|
|||
propList.addHead(x2.value); // 2 <-> 1 <-> 3
|
|||
}, |
|||
propList => { |
|||
propList.dropTail(); // 2 <-> 1
|
|||
}, |
|||
], |
|||
}; |
|||
|
|||
const contributors2: EntityPropContributorCallbacks = { |
|||
y: [ |
|||
propList => { |
|||
const y2 = propList.dropByIndex(1); // 1 <-> 3
|
|||
propList.addTail(y2.value); // 1 <-> 3 <-> 2
|
|||
}, |
|||
propList => { |
|||
propList.dropHead(); // 3 <-> 2
|
|||
}, |
|||
], |
|||
}; |
|||
|
|||
mergeWithDefaultProps(entityProps, defaults, contributors1, contributors2); |
|||
|
|||
expect(entityProps.get('x').props.toString()).toBe('2 <-> 1'); |
|||
expect(entityProps.get('y').props.toString()).toBe('3 <-> 2'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,302 @@ |
|||
import { ApplicationConfiguration, ConfigState } from '@abp/ng.core'; |
|||
import { HttpClient } from '@angular/common/http'; |
|||
import { SpectatorService } from '@ngneat/spectator'; |
|||
import { createServiceFactory } from '@ngneat/spectator/jest'; |
|||
import { NgxsModule, Store } from '@ngxs/store'; |
|||
import { of } from 'rxjs'; |
|||
import { take } from 'rxjs/operators'; |
|||
import { ePropType } from '../lib/enums/props.enum'; |
|||
import { EntityPropList } from '../lib/models/entity-props'; |
|||
import { FormPropList } from '../lib/models/form-props'; |
|||
import { ObjectExtensions } from '../lib/models/object-extensions'; |
|||
import { |
|||
getObjectExtensionEntitiesFromStore, |
|||
mapEntitiesToContributors, |
|||
} from '../lib/utils/state.util'; |
|||
|
|||
describe('State Utils', () => { |
|||
let spectator: SpectatorService<Store>; |
|||
let store: Store; |
|||
|
|||
const createService = createServiceFactory({ |
|||
service: Store, |
|||
imports: [NgxsModule.forRoot([ConfigState])], |
|||
mocks: [HttpClient], |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createService(); |
|||
store = spectator.service; |
|||
store.reset(createMockState()); |
|||
}); |
|||
|
|||
describe('#getObjectExtensionEntitiesFromStore', () => { |
|||
it('should return observable entities of an existing module', async () => { |
|||
const entities = await getObjectExtensionEntitiesFromStore(store, 'Identity').toPromise(); |
|||
expect('Role' in entities).toBe(true); |
|||
}); |
|||
|
|||
it('should return observable empty object if module does not exist', async () => { |
|||
const entities = await getObjectExtensionEntitiesFromStore(store, 'Saas').toPromise(); |
|||
expect(entities).toEqual({}); |
|||
}); |
|||
|
|||
it('should not emit when object extensions do not exist', done => { |
|||
store.reset({ ConfigState: {} }); |
|||
const emit = jest.fn(); |
|||
|
|||
getObjectExtensionEntitiesFromStore(store, 'Identity').subscribe(emit); |
|||
|
|||
setTimeout(() => { |
|||
expect(emit).not.toHaveBeenCalled(); |
|||
done(); |
|||
}, 1000); |
|||
}); |
|||
}); |
|||
|
|||
describe('#mapEntitiesToContributors', () => { |
|||
it('should return contributors from given entities', async () => { |
|||
const contributors = await of(createMockEntities()) |
|||
.pipe(mapEntitiesToContributors(store, 'AbpIdentity'), take(1)) |
|||
.toPromise(); |
|||
|
|||
const propList = new EntityPropList(); |
|||
contributors.prop.Role.forEach(callback => callback(propList)); |
|||
|
|||
expect(propList.length).toBe(3); |
|||
expect(propList.head.value.name).toBe('Title'); |
|||
expect(propList.head.next.value.name).toBe('IsHero'); |
|||
expect(propList.head.next.next.value.name).toBe('MyEnum'); |
|||
|
|||
const createFormList = new FormPropList(); |
|||
contributors.createForm.Role.forEach(callback => callback(createFormList)); |
|||
|
|||
expect(createFormList.length).toBe(2); |
|||
expect(createFormList.head.value.name).toBe('Title'); |
|||
expect(createFormList.head.next.value.name).toBe('MyEnum'); |
|||
|
|||
const editFormList = new FormPropList(); |
|||
contributors.editForm.Role.forEach(callback => callback(editFormList)); |
|||
|
|||
expect(editFormList.length).toBe(2); |
|||
expect(editFormList.head.value.name).toBe('Title'); |
|||
expect(editFormList.head.next.value.name).toBe('IsHero'); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
function createMockState(): MockState { |
|||
return { |
|||
ConfigState: { |
|||
objectExtensions: { |
|||
modules: { |
|||
Identity: { |
|||
entities: createMockEntities(), |
|||
configuration: null, |
|||
}, |
|||
}, |
|||
enums: { |
|||
'MyCompanyName.MyProjectName.MyEnum': { |
|||
fields: [ |
|||
{ |
|||
name: 'MyEnumValue0', |
|||
value: 0, |
|||
}, |
|||
{ |
|||
name: 'MyEnumValue1', |
|||
value: 1, |
|||
}, |
|||
{ |
|||
name: 'MyEnumValue2', |
|||
value: 2, |
|||
}, |
|||
], |
|||
localizationResource: null, |
|||
}, |
|||
}, |
|||
}, |
|||
localization: { |
|||
values: { |
|||
Default: {}, |
|||
AbpIdentity: {}, |
|||
}, |
|||
defaultResourceName: 'Default', |
|||
currentCulture: null, |
|||
languages: [], |
|||
}, |
|||
}, |
|||
}; |
|||
} |
|||
|
|||
function createMockEntities(): ObjectExtensions.Entities { |
|||
return { |
|||
Role: { |
|||
properties: { |
|||
Title: { |
|||
type: 'System.String', |
|||
typeSimple: ePropType.String, |
|||
displayName: null, |
|||
api: { |
|||
onGet: { |
|||
isAvailable: true, |
|||
}, |
|||
onCreate: { |
|||
isAvailable: true, |
|||
}, |
|||
onUpdate: { |
|||
isAvailable: true, |
|||
}, |
|||
}, |
|||
ui: { |
|||
onTable: { |
|||
isSortable: true, |
|||
isVisible: true, |
|||
}, |
|||
onCreateForm: { |
|||
isVisible: true, |
|||
}, |
|||
onEditForm: { |
|||
isVisible: true, |
|||
}, |
|||
}, |
|||
attributes: [ |
|||
{ |
|||
typeSimple: 'required', |
|||
config: {}, |
|||
}, |
|||
{ |
|||
typeSimple: 'stringLength', |
|||
config: { |
|||
maximumLength: 20, |
|||
minimumLength: 2, |
|||
}, |
|||
}, |
|||
], |
|||
configuration: {}, |
|||
}, |
|||
IsHero: { |
|||
type: 'System.Boolean', |
|||
typeSimple: ePropType.Boolean, |
|||
displayName: null, |
|||
api: { |
|||
onGet: { |
|||
isAvailable: true, |
|||
}, |
|||
onCreate: { |
|||
isAvailable: true, |
|||
}, |
|||
onUpdate: { |
|||
isAvailable: true, |
|||
}, |
|||
}, |
|||
ui: { |
|||
onTable: { |
|||
isSortable: false, |
|||
isVisible: true, |
|||
}, |
|||
onCreateForm: { |
|||
isVisible: false, |
|||
}, |
|||
onEditForm: { |
|||
isVisible: true, |
|||
}, |
|||
}, |
|||
attributes: [], |
|||
configuration: {}, |
|||
}, |
|||
AsOf: { |
|||
type: 'System.Date', |
|||
typeSimple: ePropType.Date, |
|||
displayName: { |
|||
name: 'Active as of', |
|||
resource: 'AbpIdentity', |
|||
}, |
|||
api: { |
|||
onGet: { |
|||
isAvailable: true, |
|||
}, |
|||
onCreate: { |
|||
isAvailable: true, |
|||
}, |
|||
onUpdate: { |
|||
isAvailable: true, |
|||
}, |
|||
}, |
|||
ui: { |
|||
onTable: { |
|||
isSortable: false, |
|||
isVisible: false, |
|||
}, |
|||
onCreateForm: { |
|||
isVisible: false, |
|||
}, |
|||
onEditForm: { |
|||
isVisible: false, |
|||
}, |
|||
}, |
|||
attributes: [], |
|||
configuration: {}, |
|||
}, |
|||
MyEnum: { |
|||
type: 'MyCompanyName.MyProjectName.MyEnum', |
|||
typeSimple: ePropType.Enum, |
|||
displayName: null, |
|||
api: { |
|||
onGet: { |
|||
isAvailable: true, |
|||
}, |
|||
onCreate: { |
|||
isAvailable: true, |
|||
}, |
|||
onUpdate: { |
|||
isAvailable: true, |
|||
}, |
|||
}, |
|||
ui: { |
|||
onTable: { |
|||
isSortable: false, |
|||
isVisible: true, |
|||
}, |
|||
onCreateForm: { |
|||
isVisible: true, |
|||
}, |
|||
onEditForm: { |
|||
isVisible: false, |
|||
}, |
|||
}, |
|||
attributes: [ |
|||
{ |
|||
typeSimple: 'required', |
|||
config: { |
|||
allowEmptyStrings: false, |
|||
}, |
|||
}, |
|||
{ |
|||
typeSimple: 'enumDataType', |
|||
config: { |
|||
enumType: 'MyCompanyName.MyProjectName.MyEnum', |
|||
dataType: 'Custom', |
|||
customDataType: 'Enumeration', |
|||
}, |
|||
}, |
|||
], |
|||
configuration: {}, |
|||
defaultValue: 2, |
|||
}, |
|||
}, |
|||
configuration: {}, |
|||
}, |
|||
User: { |
|||
properties: null, |
|||
configuration: {}, |
|||
}, |
|||
ClaimType: null, |
|||
}; |
|||
} |
|||
|
|||
interface MockState { |
|||
ConfigState: { |
|||
localization: ApplicationConfiguration.Localization; |
|||
objectExtensions: ObjectExtensions.Item; |
|||
}; |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
import { TimeAdapter } from '../lib/adapters/time.adapter'; |
|||
|
|||
describe('Time Adapter', () => { |
|||
const adapter = new TimeAdapter(); |
|||
|
|||
describe('#fromModel', () => { |
|||
const date = new Date(); |
|||
const hour = date.getHours(); |
|||
const minute = date.getMinutes(); |
|||
const second = date.getSeconds(); |
|||
|
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${null} |
|||
${null} | ${null} |
|||
${'x'} | ${null} |
|||
${'13:30:45'} | ${{ hour: 13, minute: 30, second: 45 }} |
|||
${'13:30'} | ${{ hour: 13, minute: 30, second: 0 }} |
|||
${date} | ${{ hour, minute, second }} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.fromModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
|
|||
describe('#toModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${''} |
|||
${null} | ${''} |
|||
${{ hour: 13, minute: 30, second: 0 }} | ${'13:30'} |
|||
${{ hour: 13, minute: 30, second: 45 }} | ${'13:30'} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.toModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,216 @@ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { |
|||
ToolbarAction, |
|||
ToolbarActionContributorCallback, |
|||
ToolbarActionList, |
|||
ToolbarActions, |
|||
ToolbarActionsFactory, |
|||
ToolbarComponent, |
|||
} from '../lib/models/toolbar-actions'; |
|||
|
|||
describe('ToolbarActionList', () => { |
|||
it('should inherit from LinkedList', () => { |
|||
expect(new ToolbarActionList() instanceof LinkedList).toBe(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('ToolbarActions', () => { |
|||
const add1toTail: ToolbarActionContributorCallback = actionList => actionList.addTail(1 as any); |
|||
const add2toTail: ToolbarActionContributorCallback = actionList => actionList.addTail(2 as any); |
|||
const add3toTail: ToolbarActionContributorCallback = actionList => actionList.addTail(3 as any); |
|||
const dropIndex1: ToolbarActionContributorCallback = actionList => actionList.dropByIndex(1); |
|||
|
|||
describe('#actions', () => { |
|||
test.each` |
|||
callbackList | expected |
|||
${[]} | ${''} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} |
|||
${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} |
|||
${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} |
|||
`(
|
|||
'should return $expected when given callbackList is $callbackList', |
|||
({ callbackList, expected }) => { |
|||
const creator = new ToolbarActions(callbackList); |
|||
expect(creator.actions.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#addContributor', () => { |
|||
const creator = new ToolbarActions([]); |
|||
|
|||
test.each` |
|||
callbackList | callback | expected |
|||
${[]} | ${add1toTail} | ${'1'} |
|||
${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} |
|||
${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} |
|||
`(
|
|||
'should set actions to $expected when callbackList is $callbackList and given callback is $callback', |
|||
({ callback, expected }) => { |
|||
creator.addContributor(callback); |
|||
expect(creator.actions.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe('ToolbarActionsFactory', () => { |
|||
describe('#get', () => { |
|||
it('should create and return an ToolbarActions instance', () => { |
|||
const entityActions = new ToolbarActionsFactory(); |
|||
const creator = entityActions.get(''); |
|||
|
|||
expect(creator).toBeInstanceOf(ToolbarActions); |
|||
}); |
|||
|
|||
it('should store and pass contributorCallbacks to ToolbarActionsCreator instance it returns', () => { |
|||
const entityActions = new ToolbarActionsFactory(); |
|||
const creatorX1 = entityActions.get('X'); |
|||
|
|||
expect(creatorX1).toBeInstanceOf(ToolbarActions); |
|||
expect(creatorX1.actions.toArray()).toHaveLength(0); |
|||
|
|||
creatorX1.addContributor(actionList => actionList.addTail(1 as any)); |
|||
|
|||
const creatorX2 = entityActions.get('X'); |
|||
expect(creatorX2.actions.toArray()).toHaveLength(1); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('ToolbarAction', () => { |
|||
it('should be created when options object is passed as argument', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
permission: 'PERMISSION', |
|||
visible: () => false, |
|||
icon: 'ICON', |
|||
}; |
|||
|
|||
const action = new ToolbarAction(options); |
|||
|
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action(null)).toBe(options.action()); |
|||
expect(action.permission).toBe(options.permission); |
|||
expect(action.visible(null)).toBe(options.visible()); |
|||
expect(action.icon).toBe(options.icon); |
|||
}); |
|||
|
|||
it('should be created when only required options are passed', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
}; |
|||
|
|||
const action = new ToolbarAction(options); |
|||
|
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action).toBe(options.action); |
|||
expect(action.permission).toBeUndefined(); |
|||
expect(action.visible(null)).toBe(true); |
|||
expect(action.icon).toBe(''); |
|||
}); |
|||
|
|||
describe('#create', () => { |
|||
it('should return a new instance from given options', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
}; |
|||
|
|||
const action = ToolbarAction.create(options); |
|||
|
|||
expect(action).toBeInstanceOf(ToolbarAction); |
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action).toBe(options.action); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createMany', () => { |
|||
it('should return multiple instances from given options array', () => { |
|||
const options1 = { |
|||
text: 'TEXT 1', |
|||
action: () => 'ACTION 1', |
|||
}; |
|||
const options2 = { |
|||
text: 'TEXT 2', |
|||
action: () => 'ACTION 2', |
|||
}; |
|||
|
|||
const [action1, action2] = ToolbarAction.createMany([options1, options2]); |
|||
|
|||
expect(action1).toBeInstanceOf(ToolbarAction); |
|||
expect(action1.text).toBe(options1.text); |
|||
expect(action1.action).toBe(options1.action); |
|||
expect(action2).toBeInstanceOf(ToolbarAction); |
|||
expect(action2.text).toBe(options2.text); |
|||
expect(action2.action).toBe(options2.action); |
|||
}); |
|||
}); |
|||
}); |
|||
class MockComponent1 {} |
|||
class MockComponent2 {} |
|||
|
|||
describe('ToolbarComponent', () => { |
|||
it('should be created when options object is passed as argument', () => { |
|||
const options = { |
|||
component: MockComponent1, |
|||
action: () => 'ACTION', |
|||
permission: 'PERMISSION', |
|||
visible: () => false, |
|||
}; |
|||
|
|||
const action = new ToolbarComponent(options); |
|||
|
|||
expect(action.component).toBe(options.component); |
|||
expect(action.action).toBe(options.action); |
|||
expect(action.permission).toBe(options.permission); |
|||
expect(action.visible).toBe(options.visible); |
|||
}); |
|||
|
|||
it('should be created when only required options are passed', () => { |
|||
const options = { |
|||
component: MockComponent1, |
|||
}; |
|||
|
|||
const action = new ToolbarComponent(options); |
|||
|
|||
expect(action.component).toBe(options.component); |
|||
expect(action.action()).toBeUndefined(); |
|||
expect(action.permission).toBeUndefined(); |
|||
expect(action.visible()).toBe(true); |
|||
}); |
|||
|
|||
describe('#create', () => { |
|||
it('should return a new instance from given options', () => { |
|||
const options = { |
|||
component: MockComponent1, |
|||
}; |
|||
|
|||
const action = ToolbarComponent.create(options); |
|||
|
|||
expect(action).toBeInstanceOf(ToolbarComponent); |
|||
expect(action.component).toBe(options.component); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createMany', () => { |
|||
it('should return multiple instances from given options array', () => { |
|||
const options1 = { |
|||
component: MockComponent1, |
|||
}; |
|||
const options2 = { |
|||
component: MockComponent2, |
|||
}; |
|||
|
|||
const [action1, action2] = ToolbarComponent.createMany([options1, options2]); |
|||
|
|||
expect(action1).toBeInstanceOf(ToolbarComponent); |
|||
expect(action1.component).toBe(options1.component); |
|||
expect(action2).toBeInstanceOf(ToolbarComponent); |
|||
expect(action2.component).toBe(options2.component); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,20 @@ |
|||
import { Validators } from '@angular/forms'; |
|||
import { ObjectExtensions } from '../lib/models/object-extensions'; |
|||
import { getValidatorsFromProperty } from '../lib/utils/validation.util'; |
|||
|
|||
describe('Validation Utils', () => { |
|||
describe('#getValidatorsFromProperty', () => { |
|||
it('should return a list of validators derived from property attributes', () => { |
|||
const property = { |
|||
attributes: [ |
|||
{ |
|||
typeSimple: 'emailAddress', |
|||
config: {}, |
|||
}, |
|||
], |
|||
} as ObjectExtensions.Property; |
|||
|
|||
expect(getValidatorsFromProperty(property)[0]).toBe(Validators.email); |
|||
}); |
|||
}); |
|||
}); |
|||
Loading…
Reference in new issue