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