mirror of https://github.com/abpframework/abp.git
191 changed files with 10657 additions and 111 deletions
@ -0,0 +1,36 @@ |
|||
{ |
|||
"extends": ["../../.eslintrc.json"], |
|||
"ignorePatterns": ["!**/*"], |
|||
"overrides": [ |
|||
{ |
|||
"files": ["*.ts"], |
|||
"extends": [ |
|||
"plugin:@nrwl/nx/angular", |
|||
"plugin:@angular-eslint/template/process-inline-templates" |
|||
], |
|||
"rules": { |
|||
"@angular-eslint/directive-selector": [ |
|||
"error", |
|||
{ |
|||
"type": "attribute", |
|||
"prefix": "abp", |
|||
"style": "camelCase" |
|||
} |
|||
], |
|||
"@angular-eslint/component-selector": [ |
|||
"error", |
|||
{ |
|||
"type": "element", |
|||
"prefix": "abp", |
|||
"style": "kebab-case" |
|||
} |
|||
] |
|||
} |
|||
}, |
|||
{ |
|||
"files": ["*.html"], |
|||
"extends": ["plugin:@nrwl/nx/angular-template"], |
|||
"rules": {} |
|||
} |
|||
] |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
# theme-shared |
|||
|
|||
This library was generated with [Nx](https://nx.dev). |
|||
|
|||
## Running unit tests |
|||
|
|||
Run `nx test theme-shared` to execute the unit tests. |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/libs/theme-shared/extensions", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -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,40 @@ |
|||
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; |
|||
|
|||
let date: Date; |
|||
|
|||
if (typeof value === 'string') { |
|||
date = this.dateOf(value); |
|||
} else { |
|||
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; |
|||
} |
|||
|
|||
protected dateOf(value: string): Date { |
|||
const dateUtc = new Date(Date.parse(value)); |
|||
return new Date(dateUtc.getTime() + Math.abs(dateUtc.getTimezoneOffset() * 60000)); |
|||
} |
|||
} |
|||
@ -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,76 @@ |
|||
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)" |
|||
[meridian]="meridian" |
|||
></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; |
|||
@Input() meridian = false; |
|||
|
|||
@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,138 @@ |
|||
<div class="form-group" *abpPermission="prop.permission" [ngSwitch]="getComponent(prop)"> |
|||
<ng-template ngSwitchCase="input"> |
|||
<ng-template [ngTemplateOutlet]="label"></ng-template> |
|||
<input |
|||
#field |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[autocomplete]="prop.autocomplete" |
|||
[type]="getType(prop)" |
|||
[abpDisabled]="disabled" |
|||
[readonly]="readonly" |
|||
class="form-control" |
|||
/> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="hidden"> |
|||
<input [formControlName]="prop.name" type="hidden" /> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="checkbox"> |
|||
<div class="custom-checkbox custom-control" validationTarget> |
|||
<input |
|||
#field |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="disabled" |
|||
type="checkbox" |
|||
class="custom-control-input" |
|||
/> |
|||
<ng-template |
|||
[ngTemplateOutlet]="label" |
|||
[ngTemplateOutletContext]="{ $implicit: 'custom-control-label' }" |
|||
></ng-template> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="select"> |
|||
<ng-template [ngTemplateOutlet]="label"></ng-template> |
|||
<select |
|||
#field |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="disabled" |
|||
class="custom-select form-control" |
|||
> |
|||
<option |
|||
*ngFor="let option of options$ | async; trackBy: track.by('value')" |
|||
[ngValue]="option.value" |
|||
> |
|||
{{ option.key }} |
|||
</option> |
|||
</select> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="multiselect"> |
|||
<ng-template [ngTemplateOutlet]="label"></ng-template> |
|||
<select |
|||
#field |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="disabled" |
|||
multiple="multiple" |
|||
class="custom-select form-control" |
|||
> |
|||
<option |
|||
*ngFor="let option of options$ | async; trackBy: track.by('value')" |
|||
[ngValue]="option.value" |
|||
> |
|||
{{ option.key }} |
|||
</option> |
|||
</select> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="typeahead"> |
|||
<ng-template [ngTemplateOutlet]="label"></ng-template> |
|||
<div #typeahead class="position-relative" validationStyle validationTarget> |
|||
<input |
|||
#field |
|||
[id]="prop.id" |
|||
[autocomplete]="prop.autocomplete" |
|||
[abpDisabled]="disabled" |
|||
[ngbTypeahead]="search" |
|||
[editable]="false" |
|||
[inputFormatter]="typeaheadFormatter" |
|||
[resultFormatter]="typeaheadFormatter" |
|||
[ngModelOptions]="{ standalone: true }" |
|||
[(ngModel)]="typeaheadModel" |
|||
(selectItem)="setTypeaheadValue($event.item)" |
|||
(blur)="setTypeaheadValue(typeaheadModel)" |
|||
[class.is-invalid]="typeahead.classList.contains('is-invalid')" |
|||
class="form-control" |
|||
/> |
|||
<input [formControlName]="prop.name" type="hidden" /> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="date"> |
|||
<ng-template [ngTemplateOutlet]="label"></ng-template> |
|||
<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"> |
|||
<ng-template [ngTemplateOutlet]="label"></ng-template> |
|||
<ngb-timepicker [formControlName]="prop.name"></ngb-timepicker> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="dateTime"> |
|||
<ng-template [ngTemplateOutlet]="label"></ng-template> |
|||
<abp-date-time-picker [prop]="prop" [meridian]="meridian"></abp-date-time-picker> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="textarea"> |
|||
<ng-template [ngTemplateOutlet]="label"></ng-template> |
|||
<textarea |
|||
#field |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="disabled" |
|||
[readonly]="readonly" |
|||
class="form-control" |
|||
></textarea> |
|||
</ng-template> |
|||
</div> |
|||
|
|||
<ng-template #label let-classes> |
|||
<label [htmlFor]="prop.id" [ngClass]="classes" |
|||
>{{ prop.displayName | abpLocalization }} {{ asterisk }}</label |
|||
> |
|||
</ng-template> |
|||
@ -0,0 +1,193 @@ |
|||
import { ABP, AbpValidators, ConfigStateService, TrackByService } from '@abp/ng.core'; |
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
ElementRef, |
|||
Input, |
|||
OnChanges, |
|||
Optional, |
|||
SimpleChanges, |
|||
SkipSelf, |
|||
ViewChild, |
|||
} from '@angular/core'; |
|||
import { |
|||
ControlContainer, |
|||
FormGroup, |
|||
FormGroupDirective, |
|||
ValidatorFn, |
|||
Validators, |
|||
} from '@angular/forms'; |
|||
import { NgbDateAdapter, NgbTimeAdapter } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; |
|||
import snq from 'snq'; |
|||
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 { FormProp } from '../../models/form-props'; |
|||
import { PropData } from '../../models/props'; |
|||
import { selfFactory } from '../../utils/factory.util'; |
|||
import { addTypeaheadTextSuffix } from '../../utils/typeahead.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 implements OnChanges, AfterViewInit { |
|||
@Input() data: PropData; |
|||
|
|||
@Input() prop: FormProp; |
|||
|
|||
@Input() first: boolean; |
|||
|
|||
@ViewChild('field') private fieldRef: ElementRef<HTMLElement>; |
|||
|
|||
asterisk = ''; |
|||
|
|||
options$: Observable<ABP.Option<any>[]> = of([]); |
|||
|
|||
validators: ValidatorFn[] = []; |
|||
|
|||
readonly: boolean; |
|||
|
|||
disabled: boolean; |
|||
|
|||
private readonly form: FormGroup; |
|||
|
|||
typeaheadModel: any; |
|||
|
|||
setTypeaheadValue(selectedOption: ABP.Option<string>) { |
|||
this.typeaheadModel = selectedOption || { key: null, value: null }; |
|||
const { key, value } = this.typeaheadModel; |
|||
const [keyControl, valueControl] = this.getTypeaheadControls(); |
|||
if (valueControl.value && !value) valueControl.markAsDirty(); |
|||
keyControl.setValue(key); |
|||
valueControl.setValue(value); |
|||
} |
|||
|
|||
search = (text$: Observable<string>) => |
|||
text$ |
|||
? text$.pipe( |
|||
debounceTime(300), |
|||
distinctUntilChanged(), |
|||
switchMap(text => this.prop.options(this.data, text)), |
|||
) |
|||
: of([]); |
|||
|
|||
typeaheadFormatter = (option: ABP.Option<any>) => option.key; |
|||
|
|||
get meridian() { |
|||
return ( |
|||
this.configState.getDeep('localization.currentCulture.dateTimeFormat.shortTimePattern') || '' |
|||
).includes('tt'); |
|||
} |
|||
|
|||
get isInvalid() { |
|||
const control = this.form.get(this.prop.name); |
|||
return control.touched && control.invalid; |
|||
} |
|||
|
|||
constructor( |
|||
public readonly cdRef: ChangeDetectorRef, |
|||
public readonly track: TrackByService, |
|||
protected configState: ConfigStateService, |
|||
groupDirective: FormGroupDirective, |
|||
) { |
|||
this.form = groupDirective.form; |
|||
} |
|||
|
|||
private getTypeaheadControls() { |
|||
const { name } = this.prop; |
|||
const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`; |
|||
const keyControl = |
|||
this.form.get(addTypeaheadTextSuffix(extraPropName)) || |
|||
this.form.get(addTypeaheadTextSuffix(name)); |
|||
const valueControl = this.form.get(extraPropName) || this.form.get(name); |
|||
return [keyControl, valueControl]; |
|||
} |
|||
|
|||
private setAsterisk() { |
|||
this.asterisk = this.validators.some(isRequired) ? '*' : ''; |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
if (this.first && this.fieldRef) { |
|||
this.fieldRef.nativeElement.focus(); |
|||
} |
|||
} |
|||
|
|||
getComponent(prop: FormProp): string { |
|||
switch (prop.type) { |
|||
case ePropType.Boolean: |
|||
return 'checkbox'; |
|||
case ePropType.Date: |
|||
return 'date'; |
|||
case ePropType.DateTime: |
|||
return 'dateTime'; |
|||
case ePropType.Hidden: |
|||
return 'hidden'; |
|||
case ePropType.MultiSelect: |
|||
return 'multiselect'; |
|||
case ePropType.Text: |
|||
return 'textarea'; |
|||
case ePropType.Time: |
|||
return 'time'; |
|||
case ePropType.Typeahead: |
|||
return 'typeahead'; |
|||
default: |
|||
return prop.options ? 'select' : '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'; |
|||
} |
|||
} |
|||
|
|||
ngOnChanges({ prop }: SimpleChanges) { |
|||
const currentProp = snq<FormProp>(() => prop.currentValue); |
|||
const { options, readonly, disabled, validators } = currentProp || {}; |
|||
|
|||
if (options) this.options$ = options(this.data); |
|||
if (readonly) this.readonly = readonly(this.data); |
|||
if (disabled) this.disabled = disabled(this.data); |
|||
if (validators) { |
|||
this.validators = validators(this.data); |
|||
this.setAsterisk(); |
|||
} |
|||
|
|||
const [keyControl, valueControl] = this.getTypeaheadControls(); |
|||
if (keyControl && valueControl) |
|||
this.typeaheadModel = { key: keyControl.value, value: valueControl.value }; |
|||
} |
|||
} |
|||
|
|||
function isRequired(validator: ValidatorFn) { |
|||
return validator === Validators.required || validator === AbpValidators.required; |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<ng-container *ngIf="form"> |
|||
<ng-container *abpPropData="let data; fromList: propList; withRecord: record"> |
|||
<ng-container *ngFor="let prop of propList; let first = first; 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" |
|||
[first]="first" |
|||
></abp-extensible-form-prop> |
|||
</ng-template> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
@ -0,0 +1,64 @@ |
|||
import { TrackByService } from '@abp/ng.core'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Inject, |
|||
Input, |
|||
Optional, |
|||
QueryList, |
|||
SkipSelf, |
|||
ViewChildren, |
|||
} from '@angular/core'; |
|||
import { ControlContainer, FormGroup } from '@angular/forms'; |
|||
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> { |
|||
@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; |
|||
} |
|||
|
|||
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, |
|||
) {} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
<ngx-datatable default [rows]="data" [count]="recordsTotal" [list]="list"> |
|||
<ngx-datatable-column |
|||
*ngIf="actionsTemplate || (actionList.length && hasAtLeastOnePermittedAction)" |
|||
[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="AbpUi::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 *abpPermission="prop.permission"> |
|||
<div |
|||
*ngIf="row['_' + prop.name].visible" |
|||
[innerHTML]="row['_' + prop.name].value | async" |
|||
(click)=" |
|||
prop.action && prop.action({ getInjected: getInjected, record: row, index: i }) |
|||
" |
|||
[class.pointer]="prop.action" |
|||
></div> |
|||
</ng-container> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
</ng-container> |
|||
</ngx-datatable> |
|||
@ -0,0 +1,145 @@ |
|||
import { |
|||
ListService, |
|||
ConfigStateService, |
|||
getShortDateFormat, |
|||
getShortDateShortTimeFormat, |
|||
getShortTimeFormat, |
|||
PermissionService, |
|||
} from '@abp/ng.core'; |
|||
import { formatDate } from '@angular/common'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
Inject, |
|||
Injector, |
|||
Input, |
|||
LOCALE_ID, |
|||
TemplateRef, |
|||
TrackByFunction, |
|||
Type, |
|||
InjectionToken, |
|||
InjectFlags, |
|||
SimpleChanges, |
|||
OnChanges, |
|||
} 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'; |
|||
import { EntityActionList } from '../../models/entity-actions'; |
|||
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> implements OnChanges { |
|||
protected _actionsText: string; |
|||
@Input() |
|||
set actionsText(value: string) { |
|||
this._actionsText = value; |
|||
} |
|||
get actionsText(): string { |
|||
return this._actionsText ?? (this.actionList.length > 1 ? 'AbpUi::Actions' : ''); |
|||
} |
|||
|
|||
@Input() data: R[]; |
|||
@Input() list: ListService; |
|||
@Input() recordsTotal: number; |
|||
@Input() set actionsColumnWidth(width: number) { |
|||
this.setColumnWidths(width ? Number(width) : undefined); |
|||
} |
|||
@Input() actionsTemplate: TemplateRef<any>; |
|||
|
|||
getInjected: <T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags) => T; |
|||
|
|||
readonly columnWidths: number[]; |
|||
|
|||
readonly propList: EntityPropList<R>; |
|||
|
|||
readonly actionList: EntityActionList<R>; |
|||
|
|||
readonly trackByFn: TrackByFunction<EntityProp<R>> = (_, item) => item.name; |
|||
|
|||
hasAtLeastOnePermittedAction: boolean; |
|||
|
|||
constructor( |
|||
@Inject(LOCALE_ID) private locale: string, |
|||
private config: ConfigStateService, |
|||
injector: Injector, |
|||
) { |
|||
// tslint:disable-next-line
|
|||
this.getInjected = injector.get.bind(injector); |
|||
const extensions = injector.get(ExtensionsService); |
|||
const name = injector.get(EXTENSIONS_IDENTIFIER); |
|||
this.propList = extensions.entityProps.get(name).props; |
|||
this.actionList = extensions['entityActions'].get(name) |
|||
.actions as unknown as EntityActionList<R>; |
|||
|
|||
const permissionService = injector.get(PermissionService); |
|||
this.hasAtLeastOnePermittedAction = |
|||
permissionService.filterItemsByPolicy( |
|||
this.actionList.toArray().map(action => ({ requiredPolicy: action.permission })), |
|||
).length > 0; |
|||
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, getShortDateFormat(this.config)); |
|||
case ePropType.Time: |
|||
return this.getDate(value, getShortTimeFormat(this.config)); |
|||
case ePropType.DateTime: |
|||
return this.getDate(value, getShortDateShortTimeFormat(this.config)); |
|||
default: |
|||
return value; |
|||
// More types can be handled in the future
|
|||
} |
|||
}), |
|||
); |
|||
} |
|||
|
|||
ngOnChanges({ data }: SimpleChanges) { |
|||
if (!data?.currentValue) return; |
|||
|
|||
this.data = data.currentValue.map((record, index) => { |
|||
this.propList.forEach(prop => { |
|||
const propData = { getInjected: this.getInjected, record, index } as any; |
|||
record[`_${prop.value.name}`] = { |
|||
visible: prop.value.visible(propData), |
|||
value: this.getContent(prop.value, propData), |
|||
}; |
|||
}); |
|||
|
|||
return record; |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
<div *ngIf="actionList.length > 1" 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" |
|||
[ngTemplateOutlet]="btnItem" |
|||
[ngTemplateOutletContext]="{ $implicit: action }" |
|||
> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
|
|||
<ng-container |
|||
*ngIf="actionList.length === 1" |
|||
[ngTemplateOutlet]="btnItem" |
|||
[ngTemplateOutletContext]="{ $implicit: actionList.get(0).value }" |
|||
></ng-container> |
|||
|
|||
<ng-template #btnItem let-action> |
|||
<ng-container *ngIf="action.visible(data)"> |
|||
<button |
|||
ngbDropdownItem |
|||
*abpPermission="action.permission" |
|||
(click)="action.action(data)" |
|||
type="button" |
|||
class="{{ actionList.length === 1 ? 'btn btn-primary' : '' }}" |
|||
[class.text-center]="actionList.length === 1" |
|||
> |
|||
<i [ngClass]="action.icon" [class.mr-1]="action.icon"></i> |
|||
<span *ngIf="action.icon; else ellipsis">{{ action.text | abpLocalization }}</span> |
|||
<ng-template #ellipsis> |
|||
<div abpEllipsis>{{ action.text | abpLocalization }}</div> |
|||
</ng-template> |
|||
</button> |
|||
</ng-container> |
|||
</ng-template> |
|||
@ -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,20 @@ |
|||
<div class="row justify-content-end mx-n1" id="AbpContentToolbar"> |
|||
<div class="col-auto px-1 pt-0 pt-md-2" *ngFor="let action of actionList; trackBy: trackByFn"> |
|||
<ng-container *ngIf="action.visible(data)"> |
|||
<ng-container *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> |
|||
</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,15 @@ |
|||
export const enum ePropType { |
|||
Boolean = 'boolean', |
|||
Date = 'date', |
|||
DateTime = 'dateTime', |
|||
Email = 'email', |
|||
Enum = 'enum', |
|||
Hidden = 'hidden', |
|||
MultiSelect = 'multiselect', |
|||
Number = 'number', |
|||
Password = 'password', |
|||
String = 'string', |
|||
Text = 'text', |
|||
Time = 'time', |
|||
Typeahead = 'typeahead', |
|||
} |
|||
@ -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,71 @@ |
|||
/* 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'; |
|||
import { ActionCallback } from './actions'; |
|||
|
|||
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>>; |
|||
readonly action: ActionCallback<R>; |
|||
|
|||
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])); |
|||
this.action = options.action; |
|||
} |
|||
|
|||
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' |
|||
| 'action' |
|||
>; |
|||
|
|||
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,108 @@ |
|||
import { ePropType } from '../../enums/props.enum'; |
|||
import { EntityPropList } from '../entity-props'; |
|||
import { FormPropList } from '../form-props'; |
|||
import { PropContributorCallbacks } from '../props'; |
|||
|
|||
export type DisplayNameGeneratorFn = ( |
|||
displayName: LocalizableStringDto, |
|||
fallback: LocalizableStringDto, |
|||
) => string; |
|||
|
|||
export type EntityExtensions = Record<string, EntityExtensionDto>; |
|||
|
|||
export interface EntityExtensionDto { |
|||
properties: EntityExtensionProperties; |
|||
configuration: Record<string, object>; |
|||
} |
|||
|
|||
export type EntityExtensionProperties = Record<string, ExtensionPropertyDto>; |
|||
|
|||
export interface ExtensionEnumDto { |
|||
fields: ExtensionEnumFieldDto[]; |
|||
localizationResource?: string; |
|||
transformed?: any; |
|||
} |
|||
|
|||
export interface ExtensionEnumFieldDto { |
|||
name?: string; |
|||
value: any; |
|||
} |
|||
|
|||
export interface ExtensionPropertyApiCreateDto { |
|||
isAvailable: boolean; |
|||
} |
|||
|
|||
export interface ExtensionPropertyApiDto { |
|||
onGet: ExtensionPropertyApiGetDto; |
|||
onCreate: ExtensionPropertyApiCreateDto; |
|||
onUpdate: ExtensionPropertyApiUpdateDto; |
|||
} |
|||
|
|||
export interface ExtensionPropertyApiGetDto { |
|||
isAvailable: boolean; |
|||
} |
|||
|
|||
export interface ExtensionPropertyApiUpdateDto { |
|||
isAvailable: boolean; |
|||
} |
|||
|
|||
export interface ExtensionPropertyAttributeDto { |
|||
typeSimple?: string; |
|||
config: Record<string, any>; |
|||
} |
|||
|
|||
export interface ExtensionPropertyDto { |
|||
type?: string; |
|||
typeSimple?: ePropType; |
|||
displayName: LocalizableStringDto; |
|||
api: ExtensionPropertyApiDto; |
|||
ui: ExtensionPropertyUiDto; |
|||
attributes: ExtensionPropertyAttributeDto[]; |
|||
configuration: Record<string, any>; |
|||
defaultValue: any; |
|||
} |
|||
|
|||
export interface ExtensionPropertyUiDto { |
|||
onTable: ExtensionPropertyUiTableDto; |
|||
onCreateForm: ExtensionPropertyUiFormDto; |
|||
onEditForm: ExtensionPropertyUiFormDto; |
|||
lookup?: ExtensionPropertyUiLookupDto; |
|||
} |
|||
|
|||
export interface ExtensionPropertyUiFormDto { |
|||
isVisible: boolean; |
|||
} |
|||
|
|||
export interface ExtensionPropertyUiLookupDto { |
|||
url?: string; |
|||
resultListPropertyName?: string; |
|||
displayPropertyName?: string; |
|||
valuePropertyName?: string; |
|||
filterParamName?: string; |
|||
} |
|||
|
|||
export interface ExtensionPropertyUiTableDto { |
|||
isSortable?: boolean; |
|||
isVisible: boolean; |
|||
} |
|||
|
|||
export interface LocalizableStringDto { |
|||
name?: string; |
|||
resource?: string; |
|||
} |
|||
|
|||
export interface ModuleExtensionDto { |
|||
entities: Record<string, EntityExtensionDto>; |
|||
configuration: Record<string, object>; |
|||
} |
|||
|
|||
export interface ObjectExtensionsDto { |
|||
modules: Record<string, ModuleExtensionDto>; |
|||
enums: Record<string, ExtensionEnumDto>; |
|||
} |
|||
|
|||
export interface PropContributors<T = any> { |
|||
prop: PropContributorCallbacks<EntityPropList<T>>; |
|||
createForm: PropContributorCallbacks<FormPropList<T>>; |
|||
editForm: PropContributorCallbacks<FormPropList<T>>; |
|||
} |
|||
@ -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'>, auxData?: any) => R; |
|||
export type PropPredicate<T> = (data?: Omit<PropData<T>, 'data'>, auxData?: any) => 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,56 @@ |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { |
|||
NgbDatepickerModule, |
|||
NgbDropdownModule, |
|||
NgbTimepickerModule, |
|||
NgbTypeaheadModule, |
|||
} 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, |
|||
NgbTypeaheadModule, |
|||
], |
|||
}) |
|||
export class BaseUiExtensionsModule {} |
|||
|
|||
@NgModule({ |
|||
exports: [BaseUiExtensionsModule], |
|||
imports: [BaseUiExtensionsModule], |
|||
}) |
|||
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.ExtensionEnumFieldDto[]) { |
|||
const enumObject: any = {}; |
|||
|
|||
members.forEach(({ name, value }) => { |
|||
enumObject[(enumObject[name] = value as any)] = name; |
|||
}); |
|||
|
|||
return enumObject; |
|||
} |
|||
|
|||
export function createEnumValueResolver<T = any>( |
|||
enumType: string, |
|||
lookupEnum: ObjectExtensions.ExtensionEnumDto, |
|||
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.ExtensionEnumDto, |
|||
): 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.ExtensionEnumDto, |
|||
): (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,33 @@ |
|||
import { |
|||
ApplicationLocalizationConfigurationDto, |
|||
createLocalizationPipeKeyGenerator, |
|||
} from '@abp/ng.core'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
|
|||
export function createDisplayNameLocalizationPipeKeyGenerator( |
|||
localization: ApplicationLocalizationConfigurationDto, |
|||
) { |
|||
const generateLocalizationPipeKey = createLocalizationPipeKeyGenerator(localization); |
|||
|
|||
return ( |
|||
displayName: ObjectExtensions.LocalizableStringDto, |
|||
fallback: ObjectExtensions.LocalizableStringDto, |
|||
) => { |
|||
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,200 @@ |
|||
import { |
|||
ABP, |
|||
ApplicationLocalizationConfigurationDto, |
|||
ConfigStateService, |
|||
ExtensionPropertyUiLookupDto, |
|||
} from '@abp/ng.core'; |
|||
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 { |
|||
createTypeaheadDisplayNameGenerator, |
|||
createTypeaheadOptions, |
|||
getTypeaheadType, |
|||
hasTypeaheadTextSuffix, |
|||
} from './typeahead.util'; |
|||
import { getValidatorsFromProperty } from './validation.util'; |
|||
|
|||
function selectObjectExtensions( |
|||
configState: ConfigStateService, |
|||
): Observable<ObjectExtensions.ObjectExtensionsDto> { |
|||
return configState.getOne$('objectExtensions'); |
|||
} |
|||
|
|||
function selectLocalization( |
|||
configState: ConfigStateService, |
|||
): Observable<ApplicationLocalizationConfigurationDto> { |
|||
return configState.getOne$('localization'); |
|||
} |
|||
|
|||
function selectEnums( |
|||
configState: ConfigStateService, |
|||
): Observable<Record<string, ObjectExtensions.ExtensionEnumDto>> { |
|||
return selectObjectExtensions(configState).pipe( |
|||
map((extensions: ObjectExtensions.ObjectExtensionsDto) => |
|||
Object.keys(extensions.enums).reduce((acc, key) => { |
|||
const { fields, localizationResource } = extensions.enums[key]; |
|||
acc[key] = { |
|||
fields, |
|||
localizationResource, |
|||
transformed: createEnum(fields), |
|||
}; |
|||
return acc; |
|||
}, {} as Record<string, ObjectExtensions.ExtensionEnumDto>), |
|||
), |
|||
); |
|||
} |
|||
|
|||
export function getObjectExtensionEntitiesFromStore( |
|||
configState: ConfigStateService, |
|||
moduleKey: string, |
|||
) { |
|||
return selectObjectExtensions(configState).pipe( |
|||
map(extensions => { |
|||
if (!extensions) return null; |
|||
|
|||
return (extensions.modules[moduleKey] || ({} as ObjectExtensions.ModuleExtensionDto)) |
|||
.entities; |
|||
}), |
|||
map(entities => (isUndefined(entities) ? {} : entities)), |
|||
filter<ObjectExtensions.EntityExtensions>(Boolean), |
|||
take(1), |
|||
); |
|||
} |
|||
|
|||
export function mapEntitiesToContributors<T = any>( |
|||
configState: ConfigStateService, |
|||
resource: string, |
|||
) { |
|||
return pipe( |
|||
switchMap(entities => |
|||
zip(selectLocalization(configState), selectEnums(configState)).pipe( |
|||
map(([localization, enums]) => { |
|||
const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator(localization); |
|||
|
|||
return Object.keys(entities).reduce( |
|||
(acc, key: keyof ObjectExtensions.EntityExtensions) => { |
|||
acc.prop[key] = []; |
|||
acc.createForm[key] = []; |
|||
acc.editForm[key] = []; |
|||
|
|||
const entity: ObjectExtensions.EntityExtensionDto = 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: ObjectExtensions.DisplayNameGeneratorFn, |
|||
resource: string, |
|||
enums: Record<string, ObjectExtensions.ExtensionEnumDto>, |
|||
) { |
|||
return ( |
|||
properties: ObjectExtensions.EntityExtensionProperties, |
|||
contributors: ObjectExtensions.PropContributors<T>, |
|||
key: string, |
|||
) => { |
|||
const isExtra = true; |
|||
const generateTypeaheadDisplayName = createTypeaheadDisplayNameGenerator( |
|||
generateDisplayName, |
|||
properties, |
|||
); |
|||
|
|||
Object.keys(properties).forEach((name: string) => { |
|||
const property = properties[name]; |
|||
const propName = name; |
|||
const lookup = property.ui.lookup || ({} as ExtensionPropertyUiLookupDto); |
|||
const type = getTypeaheadType(lookup, name) || getTypeFromProperty(property); |
|||
const generateDN = hasTypeaheadTextSuffix(name) |
|||
? generateTypeaheadDisplayName |
|||
: generateDisplayName; |
|||
const displayName = generateDN(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], propName) |
|||
: createExtraPropertyValueResolver<T>(propName); |
|||
|
|||
const entityProp = new EntityProp<T>({ |
|||
type, |
|||
name: propName, |
|||
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(propName, enums[property.type]); |
|||
else if (type === ePropType.Typeahead) options = createTypeaheadOptions(lookup); |
|||
|
|||
const formProp = new FormProp({ |
|||
type, |
|||
name: propName, |
|||
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.ExtensionPropertyDto): ePropType { |
|||
return (property.typeSimple.replace(/\?$/, '') as string) as ePropType; |
|||
} |
|||
|
|||
function isUndefined(obj: any): obj is undefined { |
|||
return typeof obj === 'undefined'; |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
import { ABP, ExtensionPropertyUiLookupDto, RestService } from '@abp/ng.core'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { ePropType } from '../enums/props.enum'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
import { PropCallback } from '../models/props'; |
|||
|
|||
const TYPEAHEAD_TEXT_SUFFIX = '_Text'; |
|||
const TYPEAHEAD_TEXT_SUFFIX_REGEX = /_Text$/; |
|||
|
|||
export function createTypeaheadOptions( |
|||
lookup: ExtensionPropertyUiLookupDto, |
|||
): PropCallback<any, Observable<ABP.Option<any>[]>> { |
|||
return (data, searchText) => |
|||
searchText |
|||
? data |
|||
.getInjected(RestService) |
|||
.request( |
|||
{ |
|||
method: 'GET', |
|||
url: lookup.url, |
|||
params: { |
|||
[lookup.filterParamName]: searchText, |
|||
}, |
|||
}, |
|||
{ apiName: 'Default' }, |
|||
) |
|||
.pipe( |
|||
map(response => { |
|||
const list = response[lookup.resultListPropertyName]; |
|||
const mapToOption = (item: any) => ({ |
|||
key: item[lookup.displayPropertyName], |
|||
value: item[lookup.valuePropertyName], |
|||
}); |
|||
return list.map(mapToOption); |
|||
}), |
|||
) |
|||
: of([]); |
|||
} |
|||
|
|||
export function getTypeaheadType(lookup: ExtensionPropertyUiLookupDto, name: string) { |
|||
return Boolean(lookup.url) |
|||
? ePropType.Typeahead |
|||
: name.endsWith(TYPEAHEAD_TEXT_SUFFIX) |
|||
? ePropType.Hidden |
|||
: undefined; |
|||
} |
|||
|
|||
export function createTypeaheadDisplayNameGenerator( |
|||
displayNameGeneratorFn: ObjectExtensions.DisplayNameGeneratorFn, |
|||
properties: ObjectExtensions.EntityExtensionProperties, |
|||
): ObjectExtensions.DisplayNameGeneratorFn { |
|||
return (displayName, fallback) => { |
|||
const name = removeTypeaheadTextSuffix(fallback.name); |
|||
return displayNameGeneratorFn(displayName || properties[name].displayName, { |
|||
name, |
|||
resource: fallback.resource, |
|||
}); |
|||
}; |
|||
} |
|||
|
|||
export function addTypeaheadTextSuffix(name: string) { |
|||
return name + TYPEAHEAD_TEXT_SUFFIX; |
|||
} |
|||
|
|||
export function hasTypeaheadTextSuffix(name: string) { |
|||
return TYPEAHEAD_TEXT_SUFFIX_REGEX.test(name); |
|||
} |
|||
|
|||
export function removeTypeaheadTextSuffix(name: string) { |
|||
return name.replace(TYPEAHEAD_TEXT_SUFFIX_REGEX, ''); |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { AbpValidators } from '@abp/ng.core'; |
|||
import { ValidatorFn } from '@angular/forms'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
|
|||
export function getValidatorsFromProperty( |
|||
property: ObjectExtensions.ExtensionPropertyDto, |
|||
): 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,114 @@ |
|||
import { ConfigStateService, LocalizationService } from '@abp/ng.core'; |
|||
import { BehaviorSubject } from 'rxjs'; |
|||
import { take } from 'rxjs/operators'; |
|||
import { PropData } from '../lib/models/props'; |
|||
import { createEnum, createEnumOptions, createEnumValueResolver } from '../lib/utils/enum.util'; |
|||
|
|||
const mockSessionState = { |
|||
languageChange$: new BehaviorSubject('tr'), |
|||
getLanguage: () => 'tr', |
|||
onLanguageChange$: () => new BehaviorSubject('tr'), |
|||
} as any; |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
|
|||
const mockL10n = { |
|||
values: { |
|||
Default: { |
|||
'Enum:MyEnum.foo': 'Foo', |
|||
'MyEnum.bar': 'Bar', |
|||
baz: 'Baz', |
|||
}, |
|||
}, |
|||
defaultResourceName: 'Default', |
|||
currentCulture: null, |
|||
languages: [], |
|||
}; |
|||
|
|||
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', () => { |
|||
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 service = createMockLocalizationService(); |
|||
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', () => { |
|||
it('should create a generator that returns observable options from enums', async () => { |
|||
const service = createMockLocalizationService(); |
|||
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 }, |
|||
]); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
function createMockLocalizationService() { |
|||
const configState = new ConfigStateService(); |
|||
configState.setState({ localization: mockL10n } as any); |
|||
|
|||
return new LocalizationService(mockSessionState, null, null, configState, null); |
|||
} |
|||
@ -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.inject(EXTENSIONS_IDENTIFIER); |
|||
yield spectator.inject(LocalizationService); |
|||
} |
|||
|
|||
interface Foo { |
|||
id: number; |
|||
foo: string; |
|||
extraProperties?: { |
|||
bool: boolean; |
|||
date: string; |
|||
dateTime: string; |
|||
time: string; |
|||
}; |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
import { ApplicationLocalizationConfigurationDto } from '@abp/ng.core'; |
|||
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: [], |
|||
languageFilesMap: null, |
|||
languagesMap: null, |
|||
} as ApplicationLocalizationConfigurationDto); |
|||
|
|||
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,369 @@ |
|||
import { ConfigStateService } from '@abp/ng.core'; |
|||
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'; |
|||
|
|||
const configState = new ConfigStateService(); |
|||
configState.setState(createMockState() as any); |
|||
|
|||
describe('State Utils', () => { |
|||
describe('#getObjectExtensionEntitiesFromStore', () => { |
|||
it('should return observable entities of an existing module', async () => { |
|||
const entities = await getObjectExtensionEntitiesFromStore( |
|||
configState, |
|||
'Identity', |
|||
).toPromise(); |
|||
expect('Role' in entities).toBe(true); |
|||
}); |
|||
|
|||
it('should return observable empty object if module does not exist', async () => { |
|||
const entities = await getObjectExtensionEntitiesFromStore(configState, 'Saas').toPromise(); |
|||
expect(entities).toEqual({}); |
|||
}); |
|||
|
|||
it('should not emit when object extensions do not exist', done => { |
|||
const emptyConfigState = new ConfigStateService(); |
|||
const emit = jest.fn(); |
|||
|
|||
getObjectExtensionEntitiesFromStore(emptyConfigState, '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(configState, 'AbpIdentity'), take(1)) |
|||
.toPromise(); |
|||
|
|||
const propList = new EntityPropList(); |
|||
contributors.prop.Role.forEach(callback => callback(propList)); |
|||
|
|||
expect(propList.length).toBe(4); |
|||
expect(propList.head.value.name).toBe('Title'); |
|||
expect(propList.head.next.value.name).toBe('IsHero'); |
|||
expect(propList.head.next.next.value.name).toBe('MyEnum'); |
|||
expect(propList.head.next.next.next.value.name).toBe('Foo_Text'); |
|||
|
|||
const createFormList = new FormPropList(); |
|||
contributors.createForm.Role.forEach(callback => callback(createFormList)); |
|||
|
|||
expect(createFormList.length).toBe(4); |
|||
expect(createFormList.head.value.name).toBe('Title'); |
|||
expect(createFormList.head.next.value.name).toBe('MyEnum'); |
|||
expect(createFormList.head.next.next.value.name).toBe('Foo'); |
|||
expect(createFormList.head.next.next.next.value.name).toBe('Foo_Text'); |
|||
|
|||
const editFormList = new FormPropList(); |
|||
contributors.editForm.Role.forEach(callback => callback(editFormList)); |
|||
|
|||
expect(editFormList.length).toBe(4); |
|||
expect(editFormList.head.value.name).toBe('Title'); |
|||
expect(editFormList.head.next.value.name).toBe('IsHero'); |
|||
expect(editFormList.head.next.next.value.name).toBe('Foo'); |
|||
expect(editFormList.head.next.next.next.value.name).toBe('Foo_Text'); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
function createMockState() { |
|||
return { |
|||
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(): Record<string, ObjectExtensions.EntityExtensionDto> { |
|||
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, |
|||
}, |
|||
lookup: null, |
|||
}, |
|||
attributes: [ |
|||
{ |
|||
typeSimple: 'required', |
|||
config: {}, |
|||
}, |
|||
{ |
|||
typeSimple: 'stringLength', |
|||
config: { |
|||
maximumLength: 20, |
|||
minimumLength: 2, |
|||
}, |
|||
}, |
|||
], |
|||
configuration: {}, |
|||
defaultValue: null, |
|||
}, |
|||
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, |
|||
}, |
|||
lookup: null, |
|||
}, |
|||
attributes: [], |
|||
configuration: {}, |
|||
defaultValue: null, |
|||
}, |
|||
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, |
|||
}, |
|||
lookup: null, |
|||
}, |
|||
attributes: [], |
|||
configuration: {}, |
|||
defaultValue: null, |
|||
}, |
|||
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, |
|||
}, |
|||
lookup: null, |
|||
}, |
|||
attributes: [ |
|||
{ |
|||
typeSimple: 'required', |
|||
config: { |
|||
allowEmptyStrings: false, |
|||
}, |
|||
}, |
|||
{ |
|||
typeSimple: 'enumDataType', |
|||
config: { |
|||
enumType: 'MyCompanyName.MyProjectName.MyEnum', |
|||
dataType: 'Custom', |
|||
customDataType: 'Enumeration', |
|||
}, |
|||
}, |
|||
], |
|||
configuration: {}, |
|||
defaultValue: 2, |
|||
}, |
|||
Foo: { |
|||
type: 'System.String', |
|||
typeSimple: ePropType.String, |
|||
displayName: null, |
|||
api: { |
|||
onGet: { |
|||
isAvailable: false, |
|||
}, |
|||
onCreate: { |
|||
isAvailable: true, |
|||
}, |
|||
onUpdate: { |
|||
isAvailable: true, |
|||
}, |
|||
}, |
|||
ui: { |
|||
onTable: { |
|||
isVisible: false, |
|||
}, |
|||
onCreateForm: { |
|||
isVisible: true, |
|||
}, |
|||
onEditForm: { |
|||
isVisible: true, |
|||
}, |
|||
lookup: { |
|||
url: '/api/identity/roles', |
|||
resultListPropertyName: 'items', |
|||
displayPropertyName: 'text', |
|||
valuePropertyName: 'id', |
|||
filterParamName: 'filter', |
|||
}, |
|||
}, |
|||
attributes: [], |
|||
configuration: {}, |
|||
defaultValue: null, |
|||
}, |
|||
Foo_Text: { |
|||
type: 'System.String', |
|||
typeSimple: ePropType.String, |
|||
displayName: { |
|||
name: 'Foo', |
|||
resource: '_', |
|||
}, |
|||
api: { |
|||
onGet: { |
|||
isAvailable: true, |
|||
}, |
|||
onCreate: { |
|||
isAvailable: true, |
|||
}, |
|||
onUpdate: { |
|||
isAvailable: true, |
|||
}, |
|||
}, |
|||
ui: { |
|||
onTable: { |
|||
isVisible: true, |
|||
}, |
|||
onCreateForm: { |
|||
isVisible: true, |
|||
}, |
|||
onEditForm: { |
|||
isVisible: true, |
|||
}, |
|||
lookup: { |
|||
url: null, |
|||
resultListPropertyName: 'items', |
|||
displayPropertyName: 'text', |
|||
valuePropertyName: 'id', |
|||
filterParamName: 'filter', |
|||
}, |
|||
}, |
|||
attributes: [], |
|||
configuration: {}, |
|||
defaultValue: null, |
|||
}, |
|||
}, |
|||
configuration: {}, |
|||
}, |
|||
User: { |
|||
properties: null, |
|||
configuration: {}, |
|||
}, |
|||
ClaimType: null, |
|||
}; |
|||
} |
|||
@ -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,72 @@ |
|||
import { ExtensionPropertyUiLookupDto } from '@abp/ng.core'; |
|||
import { of } from 'rxjs'; |
|||
import { createTypeaheadOptions } from '../lib/utils/typeahead.util'; |
|||
|
|||
const lookup: ExtensionPropertyUiLookupDto = { |
|||
url: 'url', |
|||
resultListPropertyName: 'list', |
|||
displayPropertyName: 'text', |
|||
valuePropertyName: 'id', |
|||
filterParamName: 'filter', |
|||
}; |
|||
|
|||
describe('Typeahead Utils', () => { |
|||
describe('#createTypeaheadOptions', () => { |
|||
it('should return observable empty array when search text does not exist', async () => { |
|||
const list = await createTypeaheadOptions(null)(null, null).toPromise(); |
|||
expect(list).toEqual([]); |
|||
}); |
|||
|
|||
it('should call request method of RestService with lookup url, filter param and search text', async () => { |
|||
const data = createData([]); |
|||
const service = data.getInjected(); |
|||
await createTypeaheadOptions(lookup)(data, 'x').toPromise(); |
|||
expect(service.request).toHaveBeenCalledTimes(1); |
|||
expect(service.request).toHaveBeenCalledWith( |
|||
{ |
|||
method: 'GET', |
|||
url: 'url', |
|||
params: { |
|||
filter: 'x', |
|||
}, |
|||
}, |
|||
{ apiName: 'Default' }, |
|||
); |
|||
}); |
|||
|
|||
it('should return options based on given lookup data', async () => { |
|||
const data = createData([ |
|||
{ |
|||
text: 'foo', |
|||
id: 'bar', |
|||
}, |
|||
{ |
|||
text: 'baz', |
|||
id: 'qux', |
|||
}, |
|||
]); |
|||
|
|||
const options = await createTypeaheadOptions(lookup)(data, 'x').toPromise(); |
|||
expect(options).toEqual([ |
|||
{ |
|||
key: 'foo', |
|||
value: 'bar', |
|||
}, |
|||
{ |
|||
key: 'baz', |
|||
value: 'qux', |
|||
}, |
|||
]); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
function createData(list: { text: string; id: string }[]): any { |
|||
const service = { request: jest.fn(() => of({ list })) }; |
|||
|
|||
return { |
|||
getInjected: () => service, |
|||
index: 0, |
|||
record: null, |
|||
}; |
|||
} |
|||
@ -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.ExtensionPropertyDto; |
|||
|
|||
expect(getValidatorsFromProperty(property)[0]).toBe(Validators.email); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,8 @@ |
|||
import { BaseUiExtensionsModule } from '@abp/ng.theme.shared/extensions'; |
|||
import { NgModule } from '@angular/core'; |
|||
|
|||
@NgModule({ |
|||
exports: [BaseUiExtensionsModule], |
|||
imports: [BaseUiExtensionsModule], |
|||
}) |
|||
export class UiExtensionsTestingModule {} |
|||
@ -0,0 +1 @@ |
|||
export * from './lib/ui-extensions-testing.module'; |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/theme-shared/extensions/testing", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
module.exports = { |
|||
displayName: 'theme-shared', |
|||
preset: '../../jest.preset.js', |
|||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], |
|||
globals: { |
|||
'ts-jest': { |
|||
tsconfig: '<rootDir>/tsconfig.spec.json', |
|||
stringifyContentPathRegex: '\\.(html|svg)$', |
|||
}, |
|||
}, |
|||
coverageDirectory: '../../coverage/libs/theme-shared', |
|||
transform: { |
|||
'^.+\\.(ts|js|html)$': 'jest-preset-angular', |
|||
}, |
|||
snapshotSerializers: [ |
|||
'jest-preset-angular/build/serializers/no-ng-attributes', |
|||
'jest-preset-angular/build/serializers/ng-snapshot', |
|||
'jest-preset-angular/build/serializers/html-comment', |
|||
], |
|||
}; |
|||
@ -0,0 +1,16 @@ |
|||
{ |
|||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/libs/theme-shared", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
}, |
|||
"allowedNonPeerDependencies": [ |
|||
"@abp/ng.core", |
|||
"@fortawesome/fontawesome-free", |
|||
"@ng-bootstrap/ng-bootstrap", |
|||
"@ngx-validate/core", |
|||
"@swimlane/ngx-datatable", |
|||
"bootstrap", |
|||
"chart.js" |
|||
] |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
{ |
|||
"name": "@abp/ng.theme.shared", |
|||
"version": "4.4.0", |
|||
"homepage": "https://abp.io", |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://github.com/abpframework/abp.git" |
|||
}, |
|||
"dependencies": { |
|||
"@abp/ng.core": "~4.4.0", |
|||
"@fortawesome/fontawesome-free": "^5.14.0", |
|||
"@ng-bootstrap/ng-bootstrap": "^7.0.0", |
|||
"@ngx-validate/core": "^0.0.13", |
|||
"@swimlane/ngx-datatable": "^17.1.0", |
|||
"bootstrap": "~4.6.0", |
|||
"chart.js": "^2.9.3", |
|||
"tslib": "^2.0.0" |
|||
}, |
|||
"publishConfig": { |
|||
"access": "public" |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
import { animate, animation, keyframes, style } from '@angular/animations'; |
|||
|
|||
export const bounceIn = animation( |
|||
[ |
|||
style({ opacity: '0', display: '{{ display }}' }), |
|||
animate( |
|||
'{{ time}} {{ easing }}', |
|||
keyframes([ |
|||
style({ opacity: '0', transform: '{{ transform }} scale(0.0)', offset: 0 }), |
|||
style({ opacity: '0', transform: '{{ transform }} scale(0.8)', offset: 0.5 }), |
|||
style({ opacity: '1', transform: '{{ transform }} scale(1.0)', offset: 1 }) |
|||
]) |
|||
) |
|||
], |
|||
{ |
|||
params: { |
|||
time: '350ms', |
|||
easing: 'cubic-bezier(.7,.31,.72,1.47)', |
|||
display: 'block', |
|||
transform: 'translate(-50%, -50%)' |
|||
} |
|||
} |
|||
); |
|||
@ -0,0 +1,90 @@ |
|||
import { |
|||
animate, |
|||
animation, |
|||
trigger, |
|||
state, |
|||
style, |
|||
transition, |
|||
useAnimation, |
|||
} from '@angular/animations'; |
|||
|
|||
export const collapseY = animation( |
|||
[ |
|||
style({ height: '*', overflow: 'hidden', 'box-sizing': 'border-box' }), |
|||
animate('{{ time }} {{ easing }}', style({ height: '0', padding: '0px' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease' } }, |
|||
); |
|||
|
|||
export const collapseYWithMargin = animation( |
|||
[ |
|||
style({ 'margin-top': '0' }), |
|||
animate('{{ time }} {{ easing }}', style({ 'margin-left': '-100%' })), |
|||
], |
|||
{ |
|||
params: { time: '500ms', easing: 'ease' }, |
|||
}, |
|||
); |
|||
|
|||
export const collapseX = animation( |
|||
[ |
|||
style({ width: '*', overflow: 'hidden', 'box-sizing': 'border-box' }), |
|||
animate('{{ time }} {{ easing }}', style({ width: '0', padding: '0px' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease' } }, |
|||
); |
|||
|
|||
export const expandY = animation( |
|||
[ |
|||
style({ height: '0', overflow: 'hidden', 'box-sizing': 'border-box' }), |
|||
animate('{{ time }} {{ easing }}', style({ height: '*', padding: '*' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease' } }, |
|||
); |
|||
|
|||
export const expandYWithMargin = animation( |
|||
[ |
|||
style({ 'margin-top': '-100%' }), |
|||
animate('{{ time }} {{ easing }}', style({ 'margin-top': '0' })), |
|||
], |
|||
{ |
|||
params: { time: '500ms', easing: 'ease' }, |
|||
}, |
|||
); |
|||
|
|||
export const expandX = animation( |
|||
[ |
|||
style({ width: '0', overflow: 'hidden', 'box-sizing': 'border-box' }), |
|||
animate('{{ time }} {{ easing }}', style({ width: '*', padding: '*' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease' } }, |
|||
); |
|||
|
|||
export const collapse = trigger('collapse', [ |
|||
state('collapsed', style({ height: '0', overflow: 'hidden' })), |
|||
state('expanded', style({ height: '*', overflow: 'hidden' })), |
|||
transition('expanded => collapsed', useAnimation(collapseY)), |
|||
transition('collapsed => expanded', useAnimation(expandY)), |
|||
]); |
|||
|
|||
export const collapseWithMargin = trigger('collapseWithMargin', [ |
|||
state('collapsed', style({ 'margin-top': '-100%' })), |
|||
state('expanded', style({ 'margin-top': '0' })), |
|||
transition('expanded => collapsed', useAnimation(collapseYWithMargin), { |
|||
params: { time: '400ms', easing: 'linear' }, |
|||
}), |
|||
transition('collapsed => expanded', useAnimation(expandYWithMargin)), |
|||
]); |
|||
|
|||
export const collapseLinearWithMargin = trigger('collapseLinearWithMargin', [ |
|||
state('collapsed', style({ 'margin-top': '-100vh' })), |
|||
state('expanded', style({ 'margin-top': '0' })), |
|||
transition( |
|||
'expanded => collapsed', |
|||
useAnimation(collapseYWithMargin, { params: { time: '200ms', easing: 'linear' } }), |
|||
), |
|||
transition( |
|||
'collapsed => expanded', |
|||
useAnimation(expandYWithMargin, { params: { time: '250ms', easing: 'linear' } }), |
|||
), |
|||
]); |
|||
@ -0,0 +1,74 @@ |
|||
import { animate, animation, style } from '@angular/animations'; |
|||
|
|||
export const fadeIn = animation([style({ opacity: '0' }), animate('{{ time}} {{ easing }}', style({ opacity: '1' }))], { |
|||
params: { time: '350ms', easing: 'ease' }, |
|||
}); |
|||
|
|||
export const fadeOut = animation( |
|||
[style({ opacity: '1' }), animate('{{ time}} {{ easing }}', style({ opacity: '0' }))], |
|||
{ params: { time: '350ms', easing: 'ease' } }, |
|||
); |
|||
|
|||
export const fadeInDown = animation( |
|||
[ |
|||
style({ opacity: '0', transform: '{{ transform }} translateY(-20px)' }), |
|||
animate('{{ time }} {{ easing }}', style({ opacity: '1', transform: '{{ transform }} translateY(0)' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease', transform: '' } }, |
|||
); |
|||
|
|||
export const fadeInUp = animation( |
|||
[ |
|||
style({ opacity: '0', transform: '{{ transform }} translateY(20px)' }), |
|||
animate('{{ time }} {{ easing }}', style({ opacity: '1', transform: '{{ transform }} translateY(0)' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease', transform: '' } }, |
|||
); |
|||
|
|||
export const fadeInLeft = animation( |
|||
[ |
|||
style({ opacity: '0', transform: '{{ transform }} translateX(20px)' }), |
|||
animate('{{ time }} {{ easing }}', style({ opacity: '1', transform: '{{ transform }} translateX(0)' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease', transform: '' } }, |
|||
); |
|||
|
|||
export const fadeInRight = animation( |
|||
[ |
|||
style({ opacity: '0', transform: '{{ transform }} translateX(-20px)' }), |
|||
animate('{{ time }} {{ easing }}', style({ opacity: '1', transform: '{{ transform }} translateX(0)' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease', transform: '' } }, |
|||
); |
|||
|
|||
export const fadeOutDown = animation( |
|||
[ |
|||
style({ opacity: '1', transform: '{{ transform }} translateY(0)' }), |
|||
animate('{{ time }} {{ easing }}', style({ opacity: '0', transform: '{{ transform }} translateY(20px)' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease', transform: '' } }, |
|||
); |
|||
|
|||
export const fadeOutUp = animation( |
|||
[ |
|||
style({ opacity: '1', transform: '{{ transform }} translateY(0)' }), |
|||
animate('{{ time }} {{ easing }}', style({ opacity: '0', transform: '{{ transform }} translateY(-20px)' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease', transform: '' } }, |
|||
); |
|||
|
|||
export const fadeOutLeft = animation( |
|||
[ |
|||
style({ opacity: '1', transform: '{{ transform }} translateX(0)' }), |
|||
animate('{{ time }} {{ easing }}', style({ opacity: '0', transform: '{{ transform }} translateX(20px)' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease', transform: '' } }, |
|||
); |
|||
|
|||
export const fadeOutRight = animation( |
|||
[ |
|||
style({ opacity: '1', transform: '{{ transform }} translateX(0)' }), |
|||
animate('{{ time }} {{ easing }}', style({ opacity: '0', transform: '{{ transform }} translateX(-20px)' })), |
|||
], |
|||
{ params: { time: '350ms', easing: 'ease', transform: '' } }, |
|||
); |
|||
@ -0,0 +1,6 @@ |
|||
export * from './bounce.animations'; |
|||
export * from './collapse.animations'; |
|||
export * from './fade.animations'; |
|||
export * from './modal.animations'; |
|||
export * from './slide.animations'; |
|||
export * from './toast.animations'; |
|||
@ -0,0 +1,12 @@ |
|||
import { transition, trigger, useAnimation } from '@angular/animations'; |
|||
import { fadeIn, fadeInDown, fadeOut } from './fade.animations'; |
|||
|
|||
export const fadeAnimation = trigger('fade', [ |
|||
transition(':enter', useAnimation(fadeIn)), |
|||
transition(':leave', useAnimation(fadeOut)), |
|||
]); |
|||
|
|||
export const dialogAnimation = trigger('dialog', [ |
|||
transition(':enter', useAnimation(fadeInDown)), |
|||
transition(':leave', useAnimation(fadeOut)), |
|||
]); |
|||
@ -0,0 +1,7 @@ |
|||
import { animate, state, style, transition, trigger, query } from '@angular/animations'; |
|||
export const slideFromBottom = trigger('slideFromBottom', [ |
|||
transition('* <=> *', [ |
|||
style({ 'margin-top': '20px', opacity: '0' }), |
|||
animate('0.2s ease-out', style({ opacity: '1', 'margin-top': '0px' })), |
|||
]), |
|||
]); |
|||
@ -0,0 +1,17 @@ |
|||
import { animate, query, style, transition, trigger } from '@angular/animations'; |
|||
|
|||
export const toastInOut = trigger('toastInOut', [ |
|||
transition('* <=> *', [ |
|||
query( |
|||
':enter', |
|||
[ |
|||
style({ opacity: 0, transform: 'translateY(20px)' }), |
|||
animate('350ms ease', style({ opacity: 1, transform: 'translateY(0)' })), |
|||
], |
|||
{ optional: true }, |
|||
), |
|||
query(':leave', animate('450ms ease', style({ opacity: 0 })), { |
|||
optional: true, |
|||
}), |
|||
]), |
|||
]); |
|||
@ -0,0 +1,13 @@ |
|||
<ol class="breadcrumb" *ngIf="segments.length"> |
|||
<li class="breadcrumb-item"> |
|||
<a routerLink="/"><i class="fa fa-home"></i> </a> |
|||
</li> |
|||
<li |
|||
*ngFor="let segment of segments; let last = last" |
|||
class="breadcrumb-item" |
|||
[class.active]="last" |
|||
aria-current="page" |
|||
> |
|||
{{ segment.name | abpLocalization }} |
|||
</li> |
|||
</ol> |
|||
@ -0,0 +1,58 @@ |
|||
import { |
|||
ABP, |
|||
getRoutePath, |
|||
RouterEvents, |
|||
RoutesService, |
|||
SubscriptionService, |
|||
TreeNode, |
|||
} from '@abp/ng.core'; |
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { map, startWith } from 'rxjs/operators'; |
|||
import { eThemeSharedRouteNames } from '../../enums'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-breadcrumb', |
|||
templateUrl: './breadcrumb.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
providers: [SubscriptionService], |
|||
}) |
|||
export class BreadcrumbComponent implements OnInit { |
|||
segments: Partial<ABP.Route>[] = []; |
|||
|
|||
constructor( |
|||
public readonly cdRef: ChangeDetectorRef, |
|||
private router: Router, |
|||
private routes: RoutesService, |
|||
private subscription: SubscriptionService, |
|||
private routerEvents: RouterEvents, |
|||
) {} |
|||
|
|||
ngOnInit(): void { |
|||
this.subscription.addOne( |
|||
this.routerEvents.getNavigationEvents('End').pipe( |
|||
// tslint:disable-next-line:deprecation
|
|||
startWith(null), |
|||
map(() => this.routes.search({ path: getRoutePath(this.router) })), |
|||
), |
|||
route => { |
|||
this.segments = []; |
|||
if (route) { |
|||
let node = { parent: route } as TreeNode<ABP.Route>; |
|||
|
|||
while (node.parent) { |
|||
node = node.parent; |
|||
const { parent, children, isLeaf, ...segment } = node; |
|||
if (!isAdministration(segment)) this.segments.unshift(segment); |
|||
} |
|||
|
|||
this.cdRef.detectChanges(); |
|||
} |
|||
}, |
|||
); |
|||
} |
|||
} |
|||
|
|||
function isAdministration(route: ABP.Route) { |
|||
return route.name === eThemeSharedRouteNames.Administration; |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
import { |
|||
Component, |
|||
EventEmitter, |
|||
Input, |
|||
Output, |
|||
ViewChild, |
|||
ElementRef, |
|||
Renderer2, |
|||
OnInit, |
|||
} from '@angular/core'; |
|||
import { ABP } from '@abp/ng.core'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-button', |
|||
template: ` |
|||
<button |
|||
#button |
|||
[id]="buttonId" |
|||
[attr.type]="buttonType" |
|||
[ngClass]="buttonClass" |
|||
[disabled]="loading || disabled" |
|||
(click.stop)="click.next($event); abpClick.next($event)" |
|||
(focus)="focus.next($event); abpFocus.next($event)" |
|||
(blur)="blur.next($event); abpBlur.next($event)" |
|||
> |
|||
<i [ngClass]="icon" class="mr-1"></i><ng-content></ng-content> |
|||
</button> |
|||
`,
|
|||
}) |
|||
export class ButtonComponent implements OnInit { |
|||
@Input() |
|||
buttonId = ''; |
|||
|
|||
@Input() |
|||
buttonClass = 'btn btn-primary'; |
|||
|
|||
@Input() |
|||
buttonType = 'button'; |
|||
|
|||
@Input() |
|||
iconClass: string; |
|||
|
|||
@Input() |
|||
loading = false; |
|||
|
|||
@Input() |
|||
disabled = false; |
|||
|
|||
@Input() |
|||
attributes: ABP.Dictionary<string>; |
|||
|
|||
// tslint:disable
|
|||
@Output() readonly click = new EventEmitter<MouseEvent>(); |
|||
|
|||
@Output() readonly focus = new EventEmitter<FocusEvent>(); |
|||
|
|||
@Output() readonly blur = new EventEmitter<FocusEvent>(); |
|||
// tslint:enable
|
|||
|
|||
@Output() readonly abpClick = new EventEmitter<MouseEvent>(); |
|||
|
|||
@Output() readonly abpFocus = new EventEmitter<FocusEvent>(); |
|||
|
|||
@Output() readonly abpBlur = new EventEmitter<FocusEvent>(); |
|||
|
|||
@ViewChild('button', { static: true }) |
|||
buttonRef: ElementRef<HTMLButtonElement>; |
|||
|
|||
get icon(): string { |
|||
return `${this.loading ? 'fa fa-spinner fa-spin' : this.iconClass || 'd-none'}`; |
|||
} |
|||
|
|||
constructor(private renderer: Renderer2) {} |
|||
|
|||
ngOnInit() { |
|||
if (this.attributes) { |
|||
Object.keys(this.attributes).forEach(key => { |
|||
this.renderer.setAttribute(this.buttonRef.nativeElement, key, this.attributes[key]); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<div |
|||
style="position:relative" |
|||
[style.width]="responsive && !width ? null : width" |
|||
[style.height]="responsive && !height ? null : height" |
|||
> |
|||
<canvas |
|||
[attr.width]="responsive && !width ? null : width" |
|||
[attr.height]="responsive && !height ? null : height" |
|||
(click)="onCanvasClick($event)" |
|||
></canvas> |
|||
</div> |
|||
@ -0,0 +1,142 @@ |
|||
import { |
|||
AfterViewInit, |
|||
Component, |
|||
ElementRef, |
|||
EventEmitter, |
|||
Input, |
|||
OnDestroy, |
|||
Output, |
|||
ChangeDetectorRef, |
|||
} from '@angular/core'; |
|||
import { BehaviorSubject } from 'rxjs'; |
|||
import { chartJsLoaded$ } from '../../utils/widget-utils'; |
|||
declare const Chart: any; |
|||
|
|||
@Component({ |
|||
selector: 'abp-chart', |
|||
templateUrl: './chart.component.html', |
|||
}) |
|||
export class ChartComponent implements AfterViewInit, OnDestroy { |
|||
@Input() type: string; |
|||
|
|||
@Input() options: any = {}; |
|||
|
|||
@Input() plugins: any[] = []; |
|||
|
|||
@Input() width: string; |
|||
|
|||
@Input() height: string; |
|||
|
|||
@Input() responsive = true; |
|||
|
|||
// tslint:disable-next-line: no-output-on-prefix
|
|||
@Output() readonly onDataSelect: EventEmitter<any> = new EventEmitter(); |
|||
|
|||
@Output() readonly initialized = new BehaviorSubject(this); |
|||
|
|||
private _initialized: boolean; |
|||
|
|||
_data: any; |
|||
|
|||
chart: any; |
|||
|
|||
constructor(public el: ElementRef, private cdRef: ChangeDetectorRef) {} |
|||
|
|||
@Input() get data(): any { |
|||
return this._data; |
|||
} |
|||
|
|||
set data(val: any) { |
|||
this._data = val; |
|||
this.reinit(); |
|||
} |
|||
|
|||
get canvas() { |
|||
return this.el.nativeElement.children[0].children[0]; |
|||
} |
|||
|
|||
get base64Image() { |
|||
return this.chart.toBase64Image(); |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
chartJsLoaded$.subscribe(() => { |
|||
this.testChartJs(); |
|||
|
|||
this.initChart(); |
|||
this._initialized = true; |
|||
}); |
|||
} |
|||
|
|||
testChartJs() { |
|||
try { |
|||
// tslint:disable-next-line: no-unused-expression
|
|||
Chart; |
|||
} catch (error) { |
|||
throw new Error(`Chart is not found. Import the Chart from app.module like shown below:
|
|||
import('chart.js'); |
|||
`);
|
|||
} |
|||
} |
|||
|
|||
onCanvasClick = event => { |
|||
if (this.chart) { |
|||
const element = this.chart.getElementAtEvent(event); |
|||
const dataset = this.chart.getDatasetAtEvent(event); |
|||
if (element && element.length && dataset) { |
|||
this.onDataSelect.emit({ |
|||
originalEvent: event, |
|||
element: element[0], |
|||
dataset, |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
initChart = () => { |
|||
const opts = this.options || {}; |
|||
opts.responsive = this.responsive; |
|||
|
|||
// allows chart to resize in responsive mode
|
|||
if (opts.responsive && (this.height || this.width)) { |
|||
opts.maintainAspectRatio = false; |
|||
} |
|||
|
|||
this.chart = new Chart(this.canvas, { |
|||
type: this.type, |
|||
data: this.data, |
|||
options: this.options, |
|||
plugins: this.plugins, |
|||
}); |
|||
|
|||
this.cdRef.detectChanges(); |
|||
}; |
|||
|
|||
generateLegend = () => { |
|||
if (this.chart) { |
|||
return this.chart.generateLegend(); |
|||
} |
|||
}; |
|||
|
|||
refresh = () => { |
|||
if (this.chart) { |
|||
this.chart.update(); |
|||
this.cdRef.detectChanges(); |
|||
} |
|||
}; |
|||
|
|||
reinit = () => { |
|||
if (this.chart) { |
|||
this.chart.destroy(); |
|||
this.initChart(); |
|||
} |
|||
}; |
|||
|
|||
ngOnDestroy() { |
|||
if (this.chart) { |
|||
this.chart.destroy(); |
|||
this._initialized = false; |
|||
this.chart = null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<div class="confirmation" *ngIf="confirmation$ | async as data"> |
|||
<div |
|||
class="confirmation-backdrop" |
|||
(click)="data.options?.dismissible ? close(dismiss) : null" |
|||
></div> |
|||
<div class="confirmation-dialog"> |
|||
<div class="icon-container" [ngClass]="data.severity" *ngIf="data.severity"> |
|||
<i class="fa icon" [ngClass]="getIconClass(data)"></i> |
|||
</div> |
|||
<div class="content"> |
|||
<h1 |
|||
class="title" |
|||
*ngIf="data.title" |
|||
[innerHTML]="data.title | abpLocalization: data.options?.titleLocalizationParams" |
|||
></h1> |
|||
<p |
|||
class="message" |
|||
*ngIf="data.message" |
|||
[innerHTML]="data.message | abpLocalization: data.options?.messageLocalizationParams" |
|||
></p> |
|||
</div> |
|||
<div class="footer"> |
|||
<button |
|||
id="cancel" |
|||
class="confirmation-button confirmation-button--reject" |
|||
[innerHTML]="data.options?.cancelText || 'AbpUi::Cancel' | abpLocalization" |
|||
*ngIf="!data?.options?.hideCancelBtn" |
|||
(click)="close(reject)" |
|||
></button> |
|||
<button |
|||
id="confirm" |
|||
class="confirmation-button confirmation-button--approve" |
|||
[innerHTML]="data.options?.yesText || 'AbpUi::Yes' | abpLocalization" |
|||
*ngIf="!data?.options?.hideYesBtn" |
|||
(click)="close(confirm)" |
|||
></button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,82 @@ |
|||
.confirmation { |
|||
position: fixed; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 1060; |
|||
.confirmation-backdrop { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
z-index: 1061 !important; |
|||
} |
|||
.confirmation-dialog { |
|||
display: flex; |
|||
flex-direction: column; |
|||
margin: 20px auto; |
|||
padding: 0; |
|||
width: 450px; |
|||
min-height: 300px; |
|||
z-index: 1062 !important; |
|||
@media screen and (max-width: 500px) { |
|||
width: 90vw; |
|||
} |
|||
.icon-container { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
margin: 0 0 10px 0; |
|||
padding: 20px; |
|||
.icon { |
|||
width: 100px; |
|||
height: 100px; |
|||
stroke-width: 1; |
|||
font-size: 80px; |
|||
text-align: center; |
|||
} |
|||
} |
|||
.content { |
|||
flex-grow: 1; |
|||
display: block; |
|||
.title { |
|||
display: block; |
|||
margin: 0; |
|||
padding: 0; |
|||
font-size: 27px; |
|||
font-weight: 600; |
|||
text-align: center; |
|||
} |
|||
.message { |
|||
display: block; |
|||
margin: 10px auto; |
|||
padding: 20px; |
|||
font-size: 16px; |
|||
font-weight: 400; |
|||
text-align: center; |
|||
} |
|||
} |
|||
.footer { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: flex-end; |
|||
margin: 10px 0 0 0; |
|||
padding: 20px; |
|||
width: 100%; |
|||
.confirmation-button { |
|||
display: inline-block; |
|||
margin: 0px 5px; |
|||
padding: 10px 20px; |
|||
border: none; |
|||
border-radius: 6px; |
|||
font-size: 14px; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
import { Component } from '@angular/core'; |
|||
import { ReplaySubject } from 'rxjs'; |
|||
import { Confirmation } from '../../models/confirmation'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-confirmation', |
|||
templateUrl: './confirmation.component.html', |
|||
styleUrls: ['./confirmation.component.scss'], |
|||
}) |
|||
export class ConfirmationComponent { |
|||
confirm = Confirmation.Status.confirm; |
|||
reject = Confirmation.Status.reject; |
|||
dismiss = Confirmation.Status.dismiss; |
|||
|
|||
confirmation$: ReplaySubject<Confirmation.DialogData>; |
|||
|
|||
clear: (status: Confirmation.Status) => void; |
|||
|
|||
close(status: Confirmation.Status) { |
|||
this.clear(status); |
|||
} |
|||
|
|||
getIconClass({ severity }: Confirmation.DialogData): string { |
|||
switch (severity) { |
|||
case 'info': |
|||
return 'fa-info-circle'; |
|||
case 'success': |
|||
return 'fa-check-circle'; |
|||
case 'warning': |
|||
return 'fa-exclamation-triangle'; |
|||
case 'error': |
|||
return 'fa-times-circle'; |
|||
default: |
|||
return 'fa-question-circle'; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
<div |
|||
#container |
|||
id="abp-http-error-container" |
|||
class="error" |
|||
[style.backgroundColor]="backgroundColor" |
|||
> |
|||
<button |
|||
*ngIf="!hideCloseIcon" |
|||
id="abp-close-button" |
|||
type="button" |
|||
class="close mr-2" |
|||
(click)="destroy()" |
|||
> |
|||
<span aria-hidden="true">×</span> |
|||
</button> |
|||
|
|||
<div *ngIf="!customComponent" class="row centered"> |
|||
<div class="col-md-12"> |
|||
<div class="error-template"> |
|||
<h1>{{ statusText }} {{ title | abpLocalization }}</h1> |
|||
<div class="error-details"> |
|||
{{ details | abpLocalization }} |
|||
</div> |
|||
<div class="error-actions"> |
|||
<a |
|||
*ngIf="isHomeShow" |
|||
(click)="destroy()" |
|||
routerLink="/" |
|||
class="btn btn-primary btn-md mt-2" |
|||
><span class="glyphicon glyphicon-home"></span> |
|||
{{ { key: '::Menu:Home', defaultValue: 'Home' } | abpLocalization }} |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,14 @@ |
|||
.error { |
|||
position: fixed; |
|||
top: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
z-index: 999999; |
|||
} |
|||
|
|||
.centered { |
|||
position: fixed; |
|||
top: 50%; |
|||
left: 50%; |
|||
transform: translate(-50%, -50%); |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
import { Config, SubscriptionService } from '@abp/ng.core'; |
|||
import { |
|||
AfterViewInit, |
|||
ApplicationRef, |
|||
Component, |
|||
ComponentFactoryResolver, |
|||
ElementRef, |
|||
EmbeddedViewRef, |
|||
Injector, |
|||
OnDestroy, |
|||
OnInit, |
|||
Type, |
|||
ViewChild, |
|||
} from '@angular/core'; |
|||
import { fromEvent, Subject } from 'rxjs'; |
|||
import { debounceTime, filter } from 'rxjs/operators'; |
|||
import snq from 'snq'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-http-error-wrapper', |
|||
templateUrl: './http-error-wrapper.component.html', |
|||
styleUrls: ['http-error-wrapper.component.scss'], |
|||
providers: [SubscriptionService], |
|||
}) |
|||
export class HttpErrorWrapperComponent implements AfterViewInit, OnDestroy, OnInit { |
|||
appRef: ApplicationRef; |
|||
|
|||
cfRes: ComponentFactoryResolver; |
|||
|
|||
injector: Injector; |
|||
|
|||
status = 0; |
|||
|
|||
title: Config.LocalizationParam = 'Oops!'; |
|||
|
|||
details: Config.LocalizationParam = 'Sorry, an error has occured.'; |
|||
|
|||
customComponent: Type<any> = null; |
|||
|
|||
destroy$: Subject<void>; |
|||
|
|||
hideCloseIcon = false; |
|||
|
|||
backgroundColor: string; |
|||
|
|||
isHomeShow = true; |
|||
|
|||
@ViewChild('container', { static: false }) |
|||
containerRef: ElementRef<HTMLDivElement>; |
|||
|
|||
get statusText(): string { |
|||
return this.status ? `[${this.status}]` : ''; |
|||
} |
|||
|
|||
constructor(private subscription: SubscriptionService) {} |
|||
|
|||
ngOnInit() { |
|||
this.backgroundColor = |
|||
snq(() => window.getComputedStyle(document.body).getPropertyValue('background-color')) || |
|||
'#fff'; |
|||
} |
|||
|
|||
ngAfterViewInit() { |
|||
if (this.customComponent) { |
|||
const customComponentRef = this.cfRes |
|||
.resolveComponentFactory(this.customComponent) |
|||
.create(this.injector); |
|||
customComponentRef.instance.errorStatus = this.status; |
|||
customComponentRef.instance.destroy$ = this.destroy$; |
|||
this.appRef.attachView(customComponentRef.hostView); |
|||
this.containerRef.nativeElement.appendChild( |
|||
(customComponentRef.hostView as EmbeddedViewRef<any>).rootNodes[0], |
|||
); |
|||
customComponentRef.changeDetectorRef.detectChanges(); |
|||
} |
|||
|
|||
const keyup$ = fromEvent(document, 'keyup').pipe( |
|||
debounceTime(150), |
|||
filter((key: KeyboardEvent) => key && key.key === 'Escape'), |
|||
); |
|||
this.subscription.addOne(keyup$, () => this.destroy()); |
|||
} |
|||
|
|||
ngOnDestroy() {} |
|||
|
|||
destroy() { |
|||
this.destroy$.next(); |
|||
this.destroy$.complete(); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
export * from './breadcrumb/breadcrumb.component'; |
|||
export * from './button/button.component'; |
|||
export * from './chart/chart.component'; |
|||
export * from './confirmation/confirmation.component'; |
|||
export * from './http-error-wrapper/http-error-wrapper.component'; |
|||
export * from './loader-bar/loader-bar.component'; |
|||
export * from './loading/loading.component'; |
|||
export * from './modal/modal.component'; |
|||
export * from './modal/modal-close.directive'; |
|||
export * from './modal/modal-ref.service'; |
|||
export * from './sort-order-icon/sort-order-icon.component'; |
|||
export * from './table-empty-message/table-empty-message.component'; |
|||
export * from './table/table.component'; |
|||
export * from './toast-container/toast-container.component'; |
|||
export * from './toast/toast.component'; |
|||
@ -0,0 +1,24 @@ |
|||
.abp-loader-bar { |
|||
left: 0; |
|||
opacity: 0; |
|||
position: fixed; |
|||
top: 0; |
|||
transition: opacity 0.4s linear 0.4s; |
|||
z-index: 99999; |
|||
|
|||
&.is-loading { |
|||
opacity: 1; |
|||
transition: none; |
|||
} |
|||
|
|||
.abp-progress { |
|||
height: 3px; |
|||
left: 0; |
|||
position: fixed; |
|||
top: 0; |
|||
|
|||
&.progressing { |
|||
transition: width 0.4s ease; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
import { HttpWaitService, RouterWaitService, SubscriptionService } from '@abp/ng.core'; |
|||
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { combineLatest, Subscription, timer } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-loader-bar', |
|||
template: ` |
|||
<div id="abp-loader-bar" [ngClass]="containerClass" [class.is-loading]="isLoading"> |
|||
<div |
|||
class="abp-progress" |
|||
[class.progressing]="progressLevel" |
|||
[style.width.vw]="progressLevel" |
|||
[ngStyle]="{ |
|||
'background-color': color, |
|||
'box-shadow': boxShadow |
|||
}" |
|||
></div> |
|||
</div> |
|||
`,
|
|||
styleUrls: ['./loader-bar.component.scss'], |
|||
providers: [SubscriptionService], |
|||
}) |
|||
export class LoaderBarComponent implements OnDestroy, OnInit { |
|||
protected _isLoading: boolean; |
|||
|
|||
@Input() |
|||
set isLoading(value: boolean) { |
|||
this._isLoading = value; |
|||
this.cdRef.detectChanges(); |
|||
} |
|||
get isLoading(): boolean { |
|||
return this._isLoading; |
|||
} |
|||
|
|||
@Input() |
|||
containerClass = 'abp-loader-bar'; |
|||
|
|||
@Input() |
|||
color = '#77b6ff'; |
|||
|
|||
progressLevel = 0; |
|||
|
|||
interval = new Subscription(); |
|||
|
|||
timer = new Subscription(); |
|||
|
|||
intervalPeriod = 350; |
|||
|
|||
stopDelay = 800; |
|||
|
|||
private readonly clearProgress = () => { |
|||
this.progressLevel = 0; |
|||
this.cdRef.detectChanges(); |
|||
}; |
|||
|
|||
private readonly reportProgress = () => { |
|||
if (this.progressLevel < 75) { |
|||
this.progressLevel += 1 + Math.random() * 9; |
|||
} else if (this.progressLevel < 90) { |
|||
this.progressLevel += 0.4; |
|||
} else if (this.progressLevel < 100) { |
|||
this.progressLevel += 0.1; |
|||
} else { |
|||
this.interval.unsubscribe(); |
|||
} |
|||
this.cdRef.detectChanges(); |
|||
}; |
|||
|
|||
get boxShadow(): string { |
|||
return `0 0 10px rgba(${this.color}, 0.5)`; |
|||
} |
|||
|
|||
constructor( |
|||
private router: Router, |
|||
private cdRef: ChangeDetectorRef, |
|||
private subscription: SubscriptionService, |
|||
private httpWaitService: HttpWaitService, |
|||
private routerWaitService: RouterWaitService, |
|||
) {} |
|||
|
|||
ngOnInit() { |
|||
this.subscribeLoading(); |
|||
} |
|||
|
|||
subscribeLoading() { |
|||
this.subscription.addOne( |
|||
combineLatest([this.httpWaitService.getLoading$(), this.routerWaitService.getLoading$()]), |
|||
([httpLoading, routerLoading]) => { |
|||
if (httpLoading || routerLoading) this.startLoading(); |
|||
else this.stopLoading(); |
|||
}, |
|||
); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.interval.unsubscribe(); |
|||
} |
|||
|
|||
startLoading() { |
|||
if (this.isLoading || !this.interval.closed) return; |
|||
|
|||
this.isLoading = true; |
|||
this.progressLevel = 0; |
|||
this.cdRef.detectChanges(); |
|||
this.interval = timer(0, this.intervalPeriod).subscribe(this.reportProgress); |
|||
this.timer.unsubscribe(); |
|||
} |
|||
|
|||
stopLoading() { |
|||
this.interval.unsubscribe(); |
|||
|
|||
this.progressLevel = 100; |
|||
this.isLoading = false; |
|||
|
|||
if (!this.timer.closed) return; |
|||
|
|||
this.timer = timer(this.stopDelay).subscribe(this.clearProgress); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
import { Component, OnInit, ViewEncapsulation } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-loading', |
|||
template: ` |
|||
<div class="abp-loading"> |
|||
<i class="fa fa-spinner fa-pulse abp-spinner"></i> |
|||
</div> |
|||
`,
|
|||
encapsulation: ViewEncapsulation.None, |
|||
styles: [ |
|||
` |
|||
.abp-loading { |
|||
position: absolute; |
|||
width: 100%; |
|||
height: 100%; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 1040; |
|||
} |
|||
|
|||
.abp-loading .abp-spinner { |
|||
position: absolute; |
|||
top: 50%; |
|||
left: 50%; |
|||
font-size: 14px; |
|||
-moz-transform: translateX(-50%) translateY(-50%); |
|||
-o-transform: translateX(-50%) translateY(-50%); |
|||
-ms-transform: translateX(-50%) translateY(-50%); |
|||
-webkit-transform: translateX(-50%) translateY(-50%); |
|||
transform: translateX(-50%) translateY(-50%); |
|||
} |
|||
`,
|
|||
], |
|||
}) |
|||
export class LoadingComponent implements OnInit { |
|||
constructor() {} |
|||
|
|||
ngOnInit() {} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
import { Directive, HostListener, Optional } from '@angular/core'; |
|||
import { ModalComponent } from './modal.component'; |
|||
|
|||
@Directive({ selector: '[abpClose]' }) |
|||
export class ModalCloseDirective { |
|||
constructor(@Optional() private modal: ModalComponent) { |
|||
if (!modal) { |
|||
console.error('Please use abpClose within an abp-modal'); |
|||
} |
|||
} |
|||
|
|||
@HostListener('click') |
|||
onClick() { |
|||
this.modal?.close(); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
import { Component, ViewChild, ViewContainerRef } from '@angular/core'; |
|||
|
|||
/** |
|||
* @deprecated To be removed in v5.0 |
|||
*/ |
|||
@Component({ |
|||
selector: 'abp-modal-container', |
|||
template: '<ng-container #container></ng-container>', |
|||
}) |
|||
export class ModalContainerComponent { |
|||
@ViewChild('container', { static: true, read: ViewContainerRef }) |
|||
container: ViewContainerRef; |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
|
|||
export type ModalDismissMode = 'hard' | 'soft'; |
|||
|
|||
export interface DismissableModal { |
|||
dismiss(mode: ModalDismissMode); |
|||
} |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class ModalRefService { |
|||
modalRefs: DismissableModal[] = []; |
|||
|
|||
register(modal: DismissableModal) { |
|||
this.modalRefs.push(modal); |
|||
} |
|||
unregister(modal: DismissableModal) { |
|||
const index = this.modalRefs.indexOf(modal); |
|||
if (index > -1) { |
|||
this.modalRefs.splice(index, 1); |
|||
} |
|||
} |
|||
|
|||
dismissAll(mode: ModalDismissMode) { |
|||
this.modalRefs.forEach(modal => modal.dismiss(mode)); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue