Browse Source

feat: add extensibility system as @abp/ng.theme.shared/extensions

pull/4474/head
Arman Ozak 6 years ago
parent
commit
30e9e6a73d
  1. 8
      npm/ng-packs/packages/theme-shared/extensions/ng-package.json
  2. 56
      npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts
  3. 29
      npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date.adapter.ts
  4. 35
      npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/time.adapter.ts
  5. 32
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/abstract-actions/abstract-actions.component.ts
  6. 74
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/date-time-picker/date-time-picker.component.ts
  7. 92
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html
  8. 80
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts
  9. 22
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.html
  10. 82
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.ts
  11. 39
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html
  12. 88
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.ts
  13. 24
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html
  14. 36
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.ts
  15. 18
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html
  16. 49
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.ts
  17. 1
      npm/ng-packs/packages/theme-shared/extensions/src/lib/constants/extra-properties.ts
  18. 19
      npm/ng-packs/packages/theme-shared/extensions/src/lib/directives/disabled.directive.ts
  19. 54
      npm/ng-packs/packages/theme-shared/extensions/src/lib/directives/prop-data.directive.ts
  20. 12
      npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts
  21. 79
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/actions.ts
  22. 53
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-actions.ts
  23. 67
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.ts
  24. 109
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/form-props.ts
  25. 96
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/internal/object-extensions.ts
  26. 3
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/object-extensions.ts
  27. 85
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts
  28. 78
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/toolbar-actions.ts
  29. 16
      npm/ng-packs/packages/theme-shared/extensions/src/lib/services/extensions.service.ts
  30. 13
      npm/ng-packs/packages/theme-shared/extensions/src/lib/tokens/extensions.token.ts
  31. 48
      npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts
  32. 49
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/actions.util.ts
  33. 73
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/enum.util.ts
  34. 3
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/factory.util.ts
  35. 61
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/form-props.util.ts
  36. 27
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/localization.util.ts
  37. 62
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/props.util.ts
  38. 174
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts
  39. 14
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/validation.util.ts
  40. 70
      npm/ng-packs/packages/theme-shared/extensions/src/public-api.ts
  41. 22
      npm/ng-packs/packages/theme-shared/extensions/src/tests/actions.spec.ts
  42. 53
      npm/ng-packs/packages/theme-shared/extensions/src/tests/actions.util.spec.ts
  43. 35
      npm/ng-packs/packages/theme-shared/extensions/src/tests/date-time.adapter.spec.ts
  44. 31
      npm/ng-packs/packages/theme-shared/extensions/src/tests/date.adapter.spec.ts
  45. 151
      npm/ng-packs/packages/theme-shared/extensions/src/tests/entity-actions.spec.ts
  46. 166
      npm/ng-packs/packages/theme-shared/extensions/src/tests/entity-props.spec.ts
  47. 133
      npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts
  48. 50
      npm/ng-packs/packages/theme-shared/extensions/src/tests/extensions.service.spec.ts
  49. 28
      npm/ng-packs/packages/theme-shared/extensions/src/tests/factory.util.spec.ts
  50. 193
      npm/ng-packs/packages/theme-shared/extensions/src/tests/form-props.spec.ts
  51. 142
      npm/ng-packs/packages/theme-shared/extensions/src/tests/form-props.util.spec.ts
  52. 31
      npm/ng-packs/packages/theme-shared/extensions/src/tests/localization.util.spec.ts
  53. 22
      npm/ng-packs/packages/theme-shared/extensions/src/tests/props.spec.ts
  54. 72
      npm/ng-packs/packages/theme-shared/extensions/src/tests/props.util.spec.ts
  55. 302
      npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts
  56. 36
      npm/ng-packs/packages/theme-shared/extensions/src/tests/time.adapter.spec.ts
  57. 216
      npm/ng-packs/packages/theme-shared/extensions/src/tests/toolbar-actions.spec.ts
  58. 20
      npm/ng-packs/packages/theme-shared/extensions/src/tests/validation.util.spec.ts

8
npm/ng-packs/packages/theme-shared/extensions/ng-package.json

@ -0,0 +1,8 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/theme-shared/extensions",
"lib": {
"entryFile": "src/public-api.ts"
},
"whitelistedNonPeerDependencies": ["@abp/ng.theme.shared"]
}

56
npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.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;

29
npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date.adapter.ts

@ -0,0 +1,29 @@
import { formatDate } from '@angular/common';
import { Injectable } from '@angular/core';
import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
@Injectable()
export class DateAdapter extends NgbDateAdapter<string> {
fromModel(value: string | Date): NgbDateStruct | null {
if (!value) return null;
const date = new Date(value);
if (isNaN((date as unknown) as number)) return null;
return {
day: date.getDate(),
month: date.getMonth() + 1,
year: date.getFullYear(),
};
}
toModel(value: NgbDateStruct | null): string {
if (!value) return '';
const date = new Date(value.year, value.month - 1, value.day);
const formattedDate = formatDate(date, 'yyyy-MM-dd', 'en');
return formattedDate;
}
}

35
npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/time.adapter.ts

@ -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));
}

32
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/abstract-actions/abstract-actions.component.ts

@ -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;

74
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/date-time-picker/date-time-picker.component.ts

@ -0,0 +1,74 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Input,
Optional,
SkipSelf,
ViewChild,
} from '@angular/core';
import { ControlContainer } from '@angular/forms';
import {
NgbDateAdapter,
NgbInputDatepicker,
NgbTimeAdapter,
NgbTimepicker,
} from '@ng-bootstrap/ng-bootstrap';
import { DateTimeAdapter } from '../../adapters/date-time.adapter';
import { FormProp } from '../../models/form-props';
import { selfFactory } from '../../utils/factory.util';
@Component({
exportAs: 'abpDateTimePicker',
selector: 'abp-date-time-picker',
template: `
<input
[id]="prop.id"
[formControlName]="prop.name"
(ngModelChange)="setTime($event)"
(click)="datepicker.open()"
(keyup.space)="datepicker.open()"
ngbDatepicker
#datepicker="ngbDatepicker"
type="text"
class="form-control"
/>
<ngb-timepicker
#timepicker
[formControlName]="prop.name"
(ngModelChange)="setDate($event)"
></ngb-timepicker>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
viewProviders: [
{
provide: ControlContainer,
useFactory: selfFactory,
deps: [[new Optional(), new SkipSelf(), ControlContainer]],
},
{
provide: NgbDateAdapter,
useClass: DateTimeAdapter,
},
{
provide: NgbTimeAdapter,
useClass: DateTimeAdapter,
},
],
})
export class DateTimePickerComponent {
@Input() prop: FormProp;
@ViewChild(NgbInputDatepicker) date: NgbInputDatepicker;
@ViewChild(NgbTimepicker) time: NgbTimepicker;
constructor(public readonly cdRef: ChangeDetectorRef) {}
setDate(datestr: string) {
this.date.writeValue(datestr);
}
setTime(datestr: string) {
this.time.writeValue(datestr);
}
}

92
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html

@ -0,0 +1,92 @@
<div class="form-group" [abpPermission]="prop.permission" [ngSwitch]="getComponent(prop)">
<ng-template ngSwitchCase="input">
<label [htmlFor]="prop.id"
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label
>
<input
[id]="prop.id"
[formControlName]="prop.name"
[autocomplete]="prop.autocomplete"
[type]="getType(prop)"
[abpDisabled]="prop.disabled(data)"
[readonly]="prop.readonly(data)"
class="form-control"
/>
</ng-template>
<ng-template ngSwitchCase="checkbox">
<div class="custom-checkbox custom-control" validationTarget>
<input
[id]="prop.id"
[formControlName]="prop.name"
[abpDisabled]="prop.disabled(data)"
type="checkbox"
class="custom-control-input"
/>
<label [htmlFor]="prop.id" class="custom-control-label"
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label
>
</div>
</ng-template>
<ng-template ngSwitchCase="select">
<label [htmlFor]="prop.id"
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label
>
<select
[id]="prop.id"
[formControlName]="prop.name"
[abpDisabled]="prop.disabled(data)"
class="custom-select form-control"
>
<option
*ngFor="let option of prop.options(data) | async; trackBy: track.by('value')"
[ngValue]="option.value"
>{{ option.key }}</option
>
</select>
</ng-template>
<ng-template ngSwitchCase="date">
<label [htmlFor]="prop.id"
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label
>
<input
[id]="prop.id"
[formControlName]="prop.name"
(click)="datepicker.open()"
(keyup.space)="datepicker.open()"
ngbDatepicker
#datepicker="ngbDatepicker"
type="text"
class="form-control"
/>
</ng-template>
<ng-template ngSwitchCase="time">
<label [htmlFor]="prop.id"
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label
>
<ngb-timepicker [formControlName]="prop.name"></ngb-timepicker>
</ng-template>
<ng-template ngSwitchCase="dateTime">
<label [htmlFor]="prop.id"
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label
>
<abp-date-time-picker [prop]="prop"></abp-date-time-picker>
</ng-template>
<ng-template ngSwitchCase="textarea">
<label [htmlFor]="prop.id"
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label
>
<textarea
[id]="prop.id"
[formControlName]="prop.name"
[abpDisabled]="prop.disabled(data)"
[readonly]="prop.readonly(data)"
class="form-control"
></textarea>
</ng-template>
</div>

80
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts

@ -0,0 +1,80 @@
import { TrackByService } from '@abp/ng.core';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Input,
Optional,
SkipSelf,
} from '@angular/core';
import { ControlContainer, Validators } from '@angular/forms';
import { NgbDateAdapter, NgbTimeAdapter } from '@ng-bootstrap/ng-bootstrap';
import { DateAdapter } from '../../adapters/date.adapter';
import { TimeAdapter } from '../../adapters/time.adapter';
import { ePropType } from '../../enums/props.enum';
import { FormProp } from '../../models/form-props';
import { PropData } from '../../models/props';
import { selfFactory } from '../../utils/factory.util';
@Component({
selector: 'abp-extensible-form-prop',
templateUrl: './extensible-form-prop.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
viewProviders: [
{
provide: ControlContainer,
useFactory: selfFactory,
deps: [[new Optional(), new SkipSelf(), ControlContainer]],
},
{ provide: NgbDateAdapter, useClass: DateAdapter },
{ provide: NgbTimeAdapter, useClass: TimeAdapter },
],
})
export class ExtensibleFormPropComponent {
@Input() data: PropData;
@Input() prop: FormProp;
constructor(public readonly cdRef: ChangeDetectorRef, public readonly track: TrackByService) {}
getAsterisk(prop: FormProp, data: PropData): string {
return prop.validators(data).some(validator => validator === Validators.required) ? '*' : '';
}
getComponent(prop: FormProp): string {
if (prop.options) return 'select';
switch (prop.type) {
case ePropType.Boolean:
return 'checkbox';
case ePropType.Date:
return 'date';
case ePropType.DateTime:
return 'dateTime';
case ePropType.Text:
return 'textarea';
case ePropType.Time:
return 'time';
default:
return 'input';
}
}
getType(prop: FormProp): string {
switch (prop.type) {
case ePropType.Date:
case ePropType.String:
return 'text';
case ePropType.Boolean:
return 'checkbox';
case ePropType.Number:
return 'number';
case ePropType.Email:
return 'email';
case ePropType.Password:
return 'password';
default:
return 'hidden';
}
}
}

22
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.html

@ -0,0 +1,22 @@
<ng-container *ngIf="form">
<ng-container *abpPropData="let data; fromList: propList; withRecord: record">
<ng-container *ngFor="let prop of propList; trackBy: track.by('name')">
<ng-container *ngIf="prop.visible(data)">
<ng-container
[formGroupName]="extraPropertiesKey"
*ngIf="extraProperties.controls[prop.name]; else tempDefault"
>
<abp-extensible-form-prop [prop]="prop" [data]="data"></abp-extensible-form-prop>
</ng-container>
<ng-template #tempDefault>
<abp-extensible-form-prop
*ngIf="form.get(prop.name)"
[prop]="prop"
[data]="data"
></abp-extensible-form-prop>
</ng-template>
</ng-container>
</ng-container>
</ng-container>
</ng-container>

82
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.ts

@ -0,0 +1,82 @@
import { TrackByService } from '@abp/ng.core';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
Input,
OnDestroy,
Optional,
QueryList,
SkipSelf,
ViewChildren,
} from '@angular/core';
import { ControlContainer, FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
import { FormPropList } from '../../models/form-props';
import { ExtensionsService } from '../../services/extensions.service';
import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token';
import { selfFactory } from '../../utils/factory.util';
import { ExtensibleFormPropComponent } from './extensible-form-prop.component';
@Component({
exportAs: 'abpExtensibleForm',
selector: 'abp-extensible-form',
templateUrl: './extensible-form.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
viewProviders: [
{
provide: ControlContainer,
useFactory: selfFactory,
deps: [[new Optional(), new SkipSelf(), ControlContainer]],
},
],
})
export class ExtensibleFormComponent<R = any> implements AfterViewInit, OnDestroy {
@ViewChildren(ExtensibleFormPropComponent)
formProps: QueryList<ExtensibleFormPropComponent>;
@Input()
set selectedRecord(record: R) {
const type = !record || JSON.stringify(record) === '{}' ? 'create' : 'edit';
this.propList = this.extensions[`${type}FormProps`].get(this.identifier).props;
this.record = record;
}
private subscription = new Subscription();
extraPropertiesKey = EXTRA_PROPERTIES_KEY;
propList: FormPropList<R>;
record: R;
get form(): FormGroup {
return (this.container ? this.container.control : { controls: {} }) as FormGroup;
}
get extraProperties(): FormGroup {
return (this.form.controls.extraProperties || { controls: {} }) as FormGroup;
}
constructor(
public readonly cdRef: ChangeDetectorRef,
public readonly track: TrackByService,
private container: ControlContainer,
private extensions: ExtensionsService,
@Inject(EXTENSIONS_IDENTIFIER) private identifier: string,
) {}
ngAfterViewInit() {
this.subscription.add(
this.form.statusChanges.pipe(debounceTime(0)).subscribe(() => {
this.formProps.forEach(prop => prop.cdRef.markForCheck());
this.cdRef.detectChanges();
}),
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}

39
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html

@ -0,0 +1,39 @@
<ngx-datatable default [rows]="data" [count]="recordsTotal" [list]="list">
<ngx-datatable-column
[name]="actionsText | abpLocalization"
[maxWidth]="columnWidths[0]"
[width]="columnWidths[0]"
[sortable]="false"
>
<ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template>
<ng-container
*ngTemplateOutlet="actionsTemplate || gridActions; context: { $implicit: row, index: i }"
></ng-container>
<ng-template #gridActions>
<abp-grid-actions
[index]="i"
[record]="row"
text="LanguageManagement::Actions"
></abp-grid-actions>
</ng-template>
</ng-template>
</ngx-datatable-column>
<ng-container *ngFor="let prop of propList; let i = index; trackBy: trackByFn">
<ngx-datatable-column
[width]="columnWidths[i + 1] || 200"
[name]="prop.displayName | abpLocalization"
[prop]="prop.name"
[sortable]="prop.sortable"
>
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template>
<ng-container
*abpPropData="let data; fromList: propList; withRecord: row; atIndex: i"
[abpPermission]="prop.permission"
>
<div *ngIf="prop.visible(data)" [innerHTML]="getContent(prop, data) | async"></div>
</ng-container>
</ng-template>
</ngx-datatable-column>
</ng-container>
</ngx-datatable>

88
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.ts

@ -0,0 +1,88 @@
import { ListService } from '@abp/ng.core';
import { formatDate } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
Inject,
Injector,
Input,
LOCALE_ID,
TemplateRef,
TrackByFunction,
} from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ePropType } from '../../enums/props.enum';
import { EntityProp, EntityPropList } from '../../models/entity-props';
import { PropData } from '../../models/props';
import { ExtensionsService } from '../../services/extensions.service';
import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token';
const DEFAULT_ACTIONS_COLUMN_WIDTH = 150;
@Component({
exportAs: 'abpExtensibleTable',
selector: 'abp-extensible-table',
templateUrl: './extensible-table.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ExtensibleTableComponent<R = any> {
@Input() actionsText: string;
@Input() data: R[];
@Input() list: ListService;
@Input() recordsTotal: number;
@Input() set actionsColumnWidth(width: number) {
this.setColumnWidths(width ? Number(width) : undefined);
}
@Input() actionsTemplate: TemplateRef<any>;
readonly columnWidths: number[];
readonly propList: EntityPropList<R>;
readonly trackByFn: TrackByFunction<EntityProp<R>> = (_, item) => item.name;
constructor(@Inject(LOCALE_ID) private locale: string, injector: Injector) {
const extensions = injector.get(ExtensionsService);
const name = injector.get(EXTENSIONS_IDENTIFIER);
this.propList = extensions.entityProps.get(name).props;
this.setColumnWidths(DEFAULT_ACTIONS_COLUMN_WIDTH);
}
private setColumnWidths(actionsColumn: number) {
const widths = [actionsColumn];
this.propList.forEach(({ value: prop }) => {
widths.push(prop.columnWidth);
});
(this.columnWidths as any) = widths;
}
private getDate(value: Date, format: string) {
return value ? formatDate(value, format, this.locale) : '';
}
private getIcon(value: boolean) {
return value
? '<div class="text-center text-success"><i class="fa fa-check"></i></div>'
: '<div class="text-center text-danger"><i class="fa fa-times"></i></div>';
}
getContent(prop: EntityProp<R>, data: PropData): Observable<string> {
return prop.valueResolver(data).pipe(
map(value => {
switch (prop.type) {
case ePropType.Boolean:
return this.getIcon(value);
case ePropType.Date:
return this.getDate(value, 'yyyy-MM-dd');
case ePropType.Time:
return this.getDate(value, 'HH:mm');
case ePropType.DateTime:
return this.getDate(value, 'yyyy-MM-dd HH:mm:ss Z');
default:
return value;
// More types can be handled in the future
}
}),
);
}
}

24
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html

@ -0,0 +1,24 @@
<div ngbDropdown container="body" class="d-inline-block">
<button
class="btn btn-primary btn-sm dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
ngbDropdownToggle
>
<i [ngClass]="icon" [class.mr-1]="icon"></i>{{ text | abpLocalization }}
</button>
<div ngbDropdownMenu>
<ng-container *ngFor="let action of actionList; trackBy: trackByFn">
<button
*ngIf="action.visible(data)"
ngbDropdownItem
[abpPermission]="action.permission"
(click)="action.action(data)"
type="button"
>
<i [ngClass]="action.icon" [class.mr-1]="action.icon"></i>
{{ action.text | abpLocalization }}
</button>
</ng-container>
</div>
</div>

36
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.ts

@ -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);
}
}

18
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html

@ -0,0 +1,18 @@
<div class="row justify-content-end mx-n1" id="AbpContentToolbar">
<div class="col-auto px-1 pt-2" *ngFor="let action of actionList; trackBy: trackByFn">
<ng-container *ngIf="action.visible(data)" [abpPermission]="action.permission">
<ng-container *ngIf="action.component as component; else button">
<ng-container
*ngComponentOutlet="component; injector: createInjector(action)"
></ng-container>
</ng-container>
<ng-template #button>
<button (click)="action.action(data)" type="button" class="btn btn-primary btn-sm">
<i [ngClass]="action.icon" [class.mr-1]="action.icon"></i>
{{ action.text | abpLocalization }}
</button>
</ng-template>
</ng-container>
</div>
</div>

49
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.ts

@ -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 };
}
}

1
npm/ng-packs/packages/theme-shared/extensions/src/lib/constants/extra-properties.ts

@ -0,0 +1 @@
export const EXTRA_PROPERTIES_KEY = 'extraProperties';

19
npm/ng-packs/packages/theme-shared/extensions/src/lib/directives/disabled.directive.ts

@ -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']();
}
}
}

54
npm/ng-packs/packages/theme-shared/extensions/src/lib/directives/prop-data.directive.ts

@ -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;

12
npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts

@ -0,0 +1,12 @@
export const enum ePropType {
Boolean = 'boolean',
Date = 'date',
DateTime = 'dateTime',
Email = 'email',
Enum = 'enum',
Number = 'number',
Password = 'password',
String = 'string',
Text = 'text',
Time = 'time',
}

79
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/actions.ts

@ -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;

53
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-actions.ts

@ -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>
>;

67
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.ts

@ -0,0 +1,67 @@
/* tslint:disable:variable-name */
import { Type } from '@angular/core';
import { Observable, of } from 'rxjs';
import { O } from 'ts-toolbelt';
import {
Prop,
PropCallback,
PropContributorCallback,
PropContributorCallbacks,
PropList,
Props,
PropsFactory,
} from './props';
export class EntityPropList<R = any> extends PropList<R, EntityProp<R>> {}
export class EntityProps<R = any> extends Props<EntityPropList<R>> {
protected _ctor: Type<EntityPropList<R>> = EntityPropList;
}
export class EntityPropsFactory<R = any> extends PropsFactory<EntityProps<R>> {
protected _ctor: Type<EntityProps<R>> = EntityProps;
}
export class EntityProp<R = any> extends Prop<R> {
readonly columnWidth: number | undefined;
readonly sortable: boolean;
readonly valueResolver: PropCallback<R, Observable<any>>;
constructor(options: EntityPropOptions<R>) {
super(
options.type,
options.name,
options.displayName,
options.permission,
options.visible,
options.isExtra,
);
this.columnWidth = options.columnWidth;
this.sortable = options.sortable || false;
this.valueResolver = options.valueResolver || (data => of(data.record[this.name]));
}
static create<R = any>(options: EntityPropOptions<R>) {
return new EntityProp<R>(options);
}
static createMany<R = any>(arrayOfOptions: EntityPropOptions<R>[]) {
return arrayOfOptions.map(EntityProp.create);
}
}
export type EntityPropOptions<R = any> = O.Optional<
O.Writable<EntityProp<R>>,
| 'permission'
| 'visible'
| 'displayName'
| 'isExtra'
| 'columnWidth'
| 'sortable'
| 'valueResolver'
>;
export type EntityPropDefaults<R = any> = Record<string, EntityProp<R>[]>;
export type EntityPropContributorCallback<R = any> = PropContributorCallback<EntityPropList<R>>;
export type EntityPropContributorCallbacks<R = any> = PropContributorCallbacks<EntityPropList<R>>;

109
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/form-props.ts

@ -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;
}

96
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/internal/object-extensions.ts

@ -0,0 +1,96 @@
import { ePropType } from '../../enums/props.enum';
import { EntityPropList } from '../entity-props';
import { FormPropList } from '../form-props';
import { PropContributorCallbacks } from '../props';
export interface PropContributors<T = any> {
prop: PropContributorCallbacks<EntityPropList<T>>;
createForm: PropContributorCallbacks<FormPropList<T>>;
editForm: PropContributorCallbacks<FormPropList<T>>;
}
export interface Config {
objectExtensions: Item;
}
export interface Item {
modules: Modules;
enums: Enums;
}
export type Modules = Configuration<Module>;
export interface Module {
configuration: Configuration;
entities: Entities;
}
export type Entities = Configuration<Entity>;
export interface Entity {
configuration: Configuration;
properties: Properties;
}
export type Properties = Configuration<Property>;
export interface Property {
type: string;
typeSimple: ePropType;
displayName: DisplayName | null;
api: Api;
ui: Ui;
attributes: Attribute[];
configuration: Configuration;
defaultValue?: any;
}
export interface DisplayName {
name: string;
resource: string;
}
export interface Api {
onGet: ApiConfig;
onCreate: ApiConfig;
onUpdate: ApiConfig;
}
export interface ApiConfig {
isAvailable: boolean;
}
export interface Ui {
onTable: UiPropConfig;
onCreateForm: UiFormConfig;
onEditForm: UiFormConfig;
}
export interface UiPropConfig {
isSortable: boolean;
isVisible: boolean;
}
export interface UiFormConfig {
isVisible: boolean;
}
export interface Attribute {
typeSimple: string;
config: Configuration;
}
export type Configuration<T = any> = Record<string, T>;
export type Enums = Record<string, Enum>;
export interface Enum {
fields: EnumMember[];
localizationResource?: string;
transformed?: any;
}
export interface EnumMember {
name: string;
value: any;
}

3
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/object-extensions.ts

@ -0,0 +1,3 @@
import * as ObjectExtensions from './internal/object-extensions';
export { ObjectExtensions };

85
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts

@ -0,0 +1,85 @@
/* tslint:disable:variable-name */
import { LinkedList } from '@abp/utils';
import { InjectFlags, InjectionToken, Type } from '@angular/core';
import { O } from 'ts-toolbelt';
import { ePropType } from '../enums/props.enum';
export abstract class PropList<R = any, A = Prop<R>> extends LinkedList<A> {}
export abstract class PropData<R = any> {
abstract getInjected: <T>(
token: Type<T> | InjectionToken<T>,
notFoundValue?: T,
flags?: InjectFlags,
) => T;
index?: number;
abstract record: R;
get data(): ReadonlyPropData<R> {
return {
getInjected: this.getInjected,
index: this.index,
record: this.record,
};
}
}
export type ReadonlyPropData<R = any> = O.Readonly<Omit<PropData<R>, 'data'>>;
export abstract class Prop<R = any> {
constructor(
public readonly type: ePropType,
public readonly name: string,
public readonly displayName: string,
public readonly permission: string,
public readonly visible: PropPredicate<R> = _ => true,
public readonly isExtra = false,
) {
this.displayName = this.displayName || this.name;
}
}
export type PropCallback<T, R = any> = (data?: Omit<PropData<T>, 'data'>) => R;
export type PropPredicate<T> = (data?: Omit<PropData<T>, 'data'>) => boolean;
export abstract class PropsFactory<C extends Props<any>> {
protected abstract _ctor: Type<C>;
private contributorCallbacks: PropContributorCallbacks<InferredPropList<C>> = {};
get(name: string): C {
this.contributorCallbacks[name] = this.contributorCallbacks[name] || [];
return new this._ctor(this.contributorCallbacks[name]);
}
}
export abstract class Props<L extends PropList> {
protected abstract _ctor: Type<L>;
get props(): L {
const propList = new this._ctor();
this.callbackList.forEach(callback => callback(propList));
return propList;
}
constructor(private readonly callbackList: PropContributorCallback<L>[]) {}
addContributor(contributeCallback: PropContributorCallback<L>) {
this.callbackList.push(contributeCallback);
}
clearContributors() {
while (this.callbackList.length) this.callbackList.pop();
}
}
export type PropContributorCallbacks<L extends PropList<any>> = Record<
string,
PropContributorCallback<L>[]
>;
export type PropContributorCallback<L extends PropList<any>> = (propList: L) => any;
type InferredPropList<C> = C extends Props<infer L> ? L : never;

78
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/toolbar-actions.ts

@ -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>
>;

16
npm/ng-packs/packages/theme-shared/extensions/src/lib/services/extensions.service.ts

@ -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>();
}

13
npm/ng-packs/packages/theme-shared/extensions/src/lib/tokens/extensions.token.ts

@ -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',
);

48
npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts

@ -0,0 +1,48 @@
import { CoreModule } from '@abp/ng.core';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import {
NgbDatepickerModule,
NgbDropdownModule,
NgbTimepickerModule,
} from '@ng-bootstrap/ng-bootstrap';
import { NgxValidateCoreModule } from '@ngx-validate/core';
import { DateTimePickerComponent } from './components/date-time-picker/date-time-picker.component';
import { ExtensibleFormPropComponent } from './components/extensible-form/extensible-form-prop.component';
import { ExtensibleFormComponent } from './components/extensible-form/extensible-form.component';
import { ExtensibleTableComponent } from './components/extensible-table/extensible-table.component';
import { GridActionsComponent } from './components/grid-actions/grid-actions.component';
import { PageToolbarComponent } from './components/page-toolbar/page-toolbar.component';
import { DisabledDirective } from './directives/disabled.directive';
import { PropDataDirective } from './directives/prop-data.directive';
@NgModule({
exports: [
DateTimePickerComponent,
PageToolbarComponent,
GridActionsComponent,
ExtensibleFormComponent,
ExtensibleTableComponent,
PropDataDirective,
DisabledDirective,
],
declarations: [
DateTimePickerComponent,
PageToolbarComponent,
GridActionsComponent,
ExtensibleFormPropComponent,
ExtensibleFormComponent,
ExtensibleTableComponent,
PropDataDirective,
DisabledDirective,
],
imports: [
CoreModule,
ThemeSharedModule,
NgxValidateCoreModule,
NgbDatepickerModule,
NgbDropdownModule,
NgbTimepickerModule,
],
})
export class UiExtensionsModule {}

49
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/actions.util.ts

@ -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;

73
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/enum.util.ts

@ -0,0 +1,73 @@
import { ABP, LocalizationService } from '@abp/ng.core';
import { merge, Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties';
import { ObjectExtensions } from '../models/object-extensions';
import { PropCallback } from '../models/props';
export function createEnum(members: ObjectExtensions.EnumMember[]) {
const enumObject: any = {};
members.forEach(({ name, value }) => {
enumObject[(enumObject[name] = value)] = name;
});
return enumObject;
}
export function createEnumValueResolver<T = any>(
enumType: string,
lookupEnum: ObjectExtensions.Enum,
propName: string,
): PropCallback<T, Observable<string>> {
return data => {
const value = data.record[EXTRA_PROPERTIES_KEY][propName];
const key = lookupEnum.transformed[value];
const l10n = data.getInjected(LocalizationService);
const localizeEnum = createEnumLocalizer(l10n, enumType, lookupEnum);
return createLocalizationStream(l10n, localizeEnum(key));
};
}
export function createEnumOptions<T = any>(
enumType: string,
lookupEnum: ObjectExtensions.Enum,
): PropCallback<T, Observable<ABP.Option<any>[]>> {
return data => {
const l10n = data.getInjected(LocalizationService);
const localizeEnum = createEnumLocalizer(l10n, enumType, lookupEnum);
return createLocalizationStream(
l10n,
lookupEnum.fields.map(({ name, value }) => ({
key: localizeEnum(name),
value,
})),
);
};
}
function createLocalizationStream(l10n: LocalizationService, mapTarget: any) {
return merge(of(null), l10n.languageChange).pipe(map(() => mapTarget));
}
function createEnumLocalizer(
l10n: LocalizationService,
enumType: string,
lookupEnum: ObjectExtensions.Enum,
): (key: string) => string {
const resource = lookupEnum.localizationResource;
const shortType = getShortEnumType(enumType);
return key =>
l10n.localizeWithFallbackSync(
[resource],
['Enum:' + shortType + '.' + key, shortType + '.' + key, key],
key,
);
}
function getShortEnumType(enumType: string): string {
return enumType.split('.').pop();
}

3
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/factory.util.ts

@ -0,0 +1,3 @@
export function selfFactory(dependency?: any) {
return dependency;
}

61
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/form-props.util.ts

@ -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;
}

27
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/localization.util.ts

@ -0,0 +1,27 @@
import { ApplicationConfiguration, createLocalizationPipeKeyGenerator } from '@abp/ng.core';
import { ObjectExtensions } from '../models/object-extensions';
export function createDisplayNameLocalizationPipeKeyGenerator(
localization: ApplicationConfiguration.Localization,
) {
const generateLocalizationPipeKey = createLocalizationPipeKeyGenerator(localization);
return (displayName: ObjectExtensions.DisplayName, fallback: ObjectExtensions.DisplayName) => {
if (displayName && displayName.name)
return generateLocalizationPipeKey(
[displayName.resource],
[displayName.name],
displayName.name,
);
const key = generateLocalizationPipeKey(
[fallback.resource],
['DisplayName:' + fallback.name],
undefined,
);
if (key) return key;
return generateLocalizationPipeKey([fallback.resource], [fallback.name], fallback.name);
};
}

62
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/props.util.ts

@ -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;

174
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts

@ -0,0 +1,174 @@
import { ABP, ApplicationConfiguration } from '@abp/ng.core';
import { createSelector, Store } from '@ngxs/store';
import { Observable, pipe, zip } from 'rxjs';
import { filter, map, switchMap, take } from 'rxjs/operators';
import { ePropType } from '../enums/props.enum';
import { EntityProp, EntityPropList } from '../models/entity-props';
import { FormProp, FormPropList } from '../models/form-props';
import { ObjectExtensions } from '../models/object-extensions';
import { PropCallback } from '../models/props';
import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util';
import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util';
import { createExtraPropertyValueResolver } from './props.util';
import { getValidatorsFromProperty } from './validation.util';
const selectConfig = (state: any) => state.ConfigState;
const selectObjectExtensions = createSelector(
[selectConfig],
(config: ObjectExtensions.Config) => config.objectExtensions,
);
const selectLocalization = createSelector(
[selectConfig],
(config: ApplicationConfiguration.Response) => config.localization,
);
const selectEnums = createSelector(
[selectObjectExtensions, selectLocalization],
(extensions: ObjectExtensions.Item) =>
Object.keys(extensions.enums).reduce((acc, key) => {
const { fields, localizationResource } = extensions.enums[key];
acc[key] = {
fields,
localizationResource,
transformed: createEnum(fields),
};
return acc;
}, {} as ObjectExtensions.Enums),
);
const createObjectExtensionEntitiesSelector = (moduleKey: ModuleKey) =>
createSelector([selectObjectExtensions], (extensions: ObjectExtensions.Item) => {
if (!extensions) return null;
return (extensions.modules[moduleKey] || ({} as ObjectExtensions.Module)).entities;
});
export function getObjectExtensionEntitiesFromStore(store: Store, moduleKey: ModuleKey) {
return store.select(createObjectExtensionEntitiesSelector(moduleKey)).pipe(
map(entities => (isUndefined(entities) ? {} : entities)),
filter<ObjectExtensions.Entities>(Boolean),
take(1),
);
}
export function mapEntitiesToContributors<T = any>(store: Store, resource: string) {
return pipe(
switchMap(entities =>
zip(store.select(selectLocalization), store.select(selectEnums)).pipe(
map(([localization, enums]) => {
const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator(localization);
return Object.keys(entities).reduce(
(acc, key: keyof ObjectExtensions.Entities) => {
acc.prop[key] = [];
acc.createForm[key] = [];
acc.editForm[key] = [];
const entity: ObjectExtensions.Entity = entities[key];
if (!entity) return acc;
const properties = entity.properties;
if (!properties) return acc;
const mapPropertiesToContributors = createPropertiesToContributorsMapper<T>(
generateDisplayName,
resource,
enums,
);
return mapPropertiesToContributors(properties, acc, key);
},
{
prop: {},
createForm: {},
editForm: {},
} as ObjectExtensions.PropContributors,
);
}),
),
),
take(1),
);
}
function createPropertiesToContributorsMapper<T = any>(
generateDisplayName: DisplayNameGeneratorFn,
resource: string,
enums: Record<string, ObjectExtensions.Enum>,
) {
return (
properties: Record<string, ObjectExtensions.Property>,
contributors: ObjectExtensions.PropContributors<T>,
key: string,
) => {
const isExtra = true;
Object.keys(properties).forEach((name: string) => {
const property = properties[name];
const type = getTypeFromProperty(property);
const displayName = generateDisplayName(property.displayName, { name, resource });
if (property.ui.onTable.isVisible) {
const sortable = Boolean(property.ui.onTable.isSortable);
const columnWidth = type === ePropType.Boolean ? 150 : 250;
const valueResolver =
type === ePropType.Enum
? createEnumValueResolver(property.type, enums[property.type], name)
: createExtraPropertyValueResolver<T>(name);
const entityProp = new EntityProp<T>({
type,
name,
displayName,
sortable,
columnWidth,
valueResolver,
isExtra,
});
const contributor = (propList: EntityPropList<T>) => propList.addTail(entityProp);
contributors.prop[key].push(contributor);
}
const isOnCreateForm = property.ui.onCreateForm.isVisible;
const isOnEditForm = property.ui.onEditForm.isVisible;
if (isOnCreateForm || isOnEditForm) {
const defaultValue = property.defaultValue;
const validators = () => getValidatorsFromProperty(property);
let options: PropCallback<any, Observable<ABP.Option<any>[]>>;
if (type === ePropType.Enum) options = createEnumOptions(name, enums[property.type]);
const formProp = new FormProp({
type,
name,
displayName,
options,
defaultValue,
validators,
isExtra,
});
const formContributor = (propList: FormPropList<T>) => propList.addTail(formProp);
if (isOnCreateForm) contributors.createForm[key].push(formContributor);
if (isOnEditForm) contributors.editForm[key].push(formContributor);
}
});
return contributors;
};
}
function getTypeFromProperty(property: ObjectExtensions.Property): ePropType {
return (property.typeSimple.replace(/\?$/, '') as string) as ePropType;
}
function isUndefined(obj: any): obj is undefined {
return typeof obj === 'undefined';
}
type DisplayNameGeneratorFn = ReturnType<typeof createDisplayNameLocalizationPipeKeyGenerator>;
type ModuleKey = keyof ObjectExtensions.Modules;

14
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/validation.util.ts

@ -0,0 +1,14 @@
import { AbpValidators } from '@abp/ng.core';
import { ValidatorFn } from '@angular/forms';
import { ObjectExtensions } from '../models/object-extensions';
export function getValidatorsFromProperty(property: ObjectExtensions.Property): ValidatorFn[] {
const validators: ValidatorFn[] = [];
property.attributes.forEach(attr => {
if (attr.typeSimple in AbpValidators)
validators.push(AbpValidators[attr.typeSimple](attr.config));
});
return validators;
}

70
npm/ng-packs/packages/theme-shared/extensions/src/public-api.ts

@ -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';

22
npm/ng-packs/packages/theme-shared/extensions/src/tests/actions.spec.ts

@ -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');
});
});
});

53
npm/ng-packs/packages/theme-shared/extensions/src/tests/actions.util.spec.ts

@ -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');
});
});
});

35
npm/ng-packs/packages/theme-shared/extensions/src/tests/date-time.adapter.spec.ts

@ -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);
});
});
});

31
npm/ng-packs/packages/theme-shared/extensions/src/tests/date.adapter.spec.ts

@ -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);
});
});
});

151
npm/ng-packs/packages/theme-shared/extensions/src/tests/entity-actions.spec.ts

@ -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);
});
});
});

166
npm/ng-packs/packages/theme-shared/extensions/src/tests/entity-props.spec.ts

@ -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);
});
});
});

133
npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts

@ -0,0 +1,133 @@
import { LocalizationService } from '@abp/ng.core';
import { Store } from '@ngxs/store';
import { Subject } from 'rxjs';
import { take } from 'rxjs/operators';
import { PropData } from '../lib/models/props';
import { createEnum, createEnumOptions, createEnumValueResolver } from '../lib/utils/enum.util';
const fields = [
{ name: 'foo', value: 1 },
{ name: 'bar', value: 2 },
{ name: 'baz', value: 3 },
];
class MockPropData<R = any> extends PropData<R> {
getInjected: PropData<R>['getInjected'];
constructor(public readonly record: R) {
super();
}
}
describe('Enum Utils', () => {
describe('#createEnum', () => {
const enumFromFields = createEnum(fields);
test.each`
key | expected
${'foo'} | ${1}
${'bar'} | ${2}
${'baz'} | ${3}
${1} | ${'foo'}
${2} | ${'bar'}
${3} | ${'baz'}
`('should create an enum that returns $expected when $key is accessed', ({ key, expected }) => {
expect(enumFromFields[key]).toBe(expected);
});
});
describe('#createEnumValueResolver', () => {
const service = new LocalizationService(
new Subject().asObservable(),
({
selectSnapshot: () => ({
values: {
Default: {
'Enum:MyEnum.foo': 'Foo',
'MyEnum.bar': 'Bar',
baz: 'Baz',
},
},
defaultResourceName: 'Default',
currentCulture: null,
languages: [],
}),
} as unknown) as Store,
null,
null,
null,
);
test.each`
value | expected
${1} | ${'Foo'}
${2} | ${'Bar'}
${3} | ${'Baz'}
`(
'should create a resolver that returns observable $expected when enum value is $value',
async ({ value, expected }) => {
const valueResolver = createEnumValueResolver(
'MyCompanyName.MyProjectName.MyEnum',
{
fields,
localizationResource: null,
transformed: createEnum(fields),
},
'EnumProp',
);
const propData = new MockPropData({ extraProperties: { EnumProp: value } });
propData.getInjected = () => service as any;
const resolved = await valueResolver(propData)
.pipe(take(1))
.toPromise();
expect(resolved).toBe(expected);
},
);
});
describe('#createEnumOptions', () => {
const service = new LocalizationService(
new Subject().asObservable(),
({
selectSnapshot: () => ({
values: {
Default: {
'Enum:MyEnum.foo': 'Foo',
'MyEnum.bar': 'Bar',
baz: 'Baz',
},
},
defaultResourceName: 'Default',
currentCulture: null,
languages: [],
}),
} as unknown) as Store,
null,
null,
null,
);
it('should create a generator that returns observable options from enums', async () => {
const options = createEnumOptions('MyCompanyName.MyProjectName.MyEnum', {
fields,
localizationResource: null,
transformed: createEnum(fields),
});
const propData = new MockPropData({});
propData.getInjected = () => service as any;
const resolved = await options(propData)
.pipe(take(1))
.toPromise();
expect(resolved).toEqual([
{ key: 'Foo', value: 1 },
{ key: 'Bar', value: 2 },
{ key: 'Baz', value: 3 },
]);
});
});
});

50
npm/ng-packs/packages/theme-shared/extensions/src/tests/extensions.service.spec.ts

@ -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);
});
});
});

28
npm/ng-packs/packages/theme-shared/extensions/src/tests/factory.util.spec.ts

@ -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);
});
});
});

193
npm/ng-packs/packages/theme-shared/extensions/src/tests/form-props.spec.ts

@ -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);
});
});
});

142
npm/ng-packs/packages/theme-shared/extensions/src/tests/form-props.util.spec.ts

@ -0,0 +1,142 @@
import { LocalizationService } from '@abp/ng.core';
import { Injector } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { ePropType } from '../lib/enums/props.enum';
import { FormProp, FormPropData } from '../lib/models/form-props';
import { ExtensionsService } from '../lib/services/extensions.service';
import { EXTENSIONS_IDENTIFIER } from '../lib/tokens/extensions.token';
import { generateFormFromProps } from '../lib/utils/form-props.util';
describe('Form Prop Utils', () => {
describe('#generateFormFromProps', () => {
let spectator: SpectatorService<ExtensionsService<Foo>>;
let injector: Injector;
const identifier = 'X';
const createService = createServiceFactory({
service: ExtensionsService,
providers: [
{
provide: EXTENSIONS_IDENTIFIER,
useValue: identifier,
},
{
provide: LocalizationService,
useValue: { currentLang: 'en' },
},
],
});
beforeEach(() => {
spectator = createService();
const props = FormProp.createMany<Foo>([
{
type: ePropType.String,
name: 'foo',
validators: () => [Validators.required],
defaultValue: 'bar',
},
{
type: ePropType.Boolean,
name: 'bool',
},
{
type: ePropType.Date,
name: 'date',
},
{
type: ePropType.DateTime,
name: 'dateTime',
},
{
type: ePropType.Time,
name: 'time',
},
]);
spectator.service.createFormProps
.get(identifier)
.addContributor(propList => propList.addManyTail(props));
spectator.service.editFormProps
.get(identifier)
.addContributor(propList => propList.addManyTail(props));
const generator = getInjected(spectator);
injector = {
get: () => generator.next().value as any,
};
});
it('should return a blank FormGroup instance', () => {
const data = new FormPropData<Foo>(injector, null);
const formGroup = generateFormFromProps(data);
expect(formGroup).toBeInstanceOf(FormGroup);
expect(formGroup.value.foo).toBe('bar');
const formControl = formGroup.get('foo');
expect(formControl).toBeInstanceOf(FormControl);
expect(formControl.valid).toBe(true);
});
it('should return a prefilled FormGroup instance', () => {
const data = new FormPropData<Foo>(injector, { id: 1, foo: null });
const formGroup = generateFormFromProps(data);
expect(formGroup).toBeInstanceOf(FormGroup);
expect(formGroup.value.foo).toBe(null);
const formControl = formGroup.get('foo');
expect(formControl).toBeInstanceOf(FormControl);
expect(formControl.invalid).toBe(true);
});
it('should add a FormGroup named extraProperties', () => {
const data = new FormPropData<Foo>(injector, null);
const formGroup = generateFormFromProps(data);
const extraPropertiesGroup = formGroup.get('extraProperties');
expect(extraPropertiesGroup).toBeInstanceOf(FormGroup);
});
it('should add extraProperties to extraProperties FormGroup', () => {
const data = new FormPropData<Foo>(injector, {
id: 1,
foo: undefined,
extraProperties: {
bool: true,
date: '03/30/2002',
dateTime: '2002-03-30 13:30:59Z',
time: '13:30:59',
},
});
const formGroup = generateFormFromProps(data);
const extraPropertiesGroup = formGroup.get('extraProperties');
expect(extraPropertiesGroup.value).toEqual({
bool: true,
date: '2002-03-30',
dateTime: '2002-03-30T13:30:59.000Z',
time: '13:30',
});
});
});
});
function* getInjected(spectator: SpectatorService<ExtensionsService>) {
yield spectator.service;
yield spectator.get(EXTENSIONS_IDENTIFIER);
yield spectator.get(LocalizationService);
}
interface Foo {
id: number;
foo: string;
extraProperties?: {
bool: boolean;
date: string;
dateTime: string;
time: string;
};
}

31
npm/ng-packs/packages/theme-shared/extensions/src/tests/localization.util.spec.ts

@ -0,0 +1,31 @@
import { createDisplayNameLocalizationPipeKeyGenerator } from '../lib/utils/localization.util';
describe('Localization Utils', () => {
describe('#createDisplayNameLocalizationPipeKeyGenerator', () => {
const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator({
values: {
Foo: { Bar: 'Bar', 'DisplayName:Bar': 'Bar' },
Default: { Bar: 'Bar', 'DisplayName:Bar': 'Bar' },
},
defaultResourceName: 'Default',
currentCulture: null,
languages: [],
});
test.each`
displayName | fallback | expected
${{ name: 'Bar', resource: 'Foo' }} | ${null} | ${'Foo::Bar'}
${{ name: 'Baz', resource: 'Foo' }} | ${null} | ${'Baz'}
${null} | ${{ name: 'Bar', resource: 'Foo' }} | ${'Foo::DisplayName:Bar'}
${null} | ${{ name: 'Bar', resource: 'Default' }} | ${'Default::DisplayName:Bar'}
${null} | ${{ name: 'Baz', resource: 'Default' }} | ${'Baz'}
`(
'should return $expected when diplay name is $displayName and fallback is $fallback',
({ displayName, fallback, expected }) => {
const result = generateDisplayName(displayName, fallback);
expect(result).toBe(expected);
},
);
});
});

22
npm/ng-packs/packages/theme-shared/extensions/src/tests/props.spec.ts

@ -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');
});
});
});

72
npm/ng-packs/packages/theme-shared/extensions/src/tests/props.util.spec.ts

@ -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');
});
});
});

302
npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts

@ -0,0 +1,302 @@
import { ApplicationConfiguration, ConfigState } from '@abp/ng.core';
import { HttpClient } from '@angular/common/http';
import { SpectatorService } from '@ngneat/spectator';
import { createServiceFactory } from '@ngneat/spectator/jest';
import { NgxsModule, Store } from '@ngxs/store';
import { of } from 'rxjs';
import { take } from 'rxjs/operators';
import { ePropType } from '../lib/enums/props.enum';
import { EntityPropList } from '../lib/models/entity-props';
import { FormPropList } from '../lib/models/form-props';
import { ObjectExtensions } from '../lib/models/object-extensions';
import {
getObjectExtensionEntitiesFromStore,
mapEntitiesToContributors,
} from '../lib/utils/state.util';
describe('State Utils', () => {
let spectator: SpectatorService<Store>;
let store: Store;
const createService = createServiceFactory({
service: Store,
imports: [NgxsModule.forRoot([ConfigState])],
mocks: [HttpClient],
});
beforeEach(() => {
spectator = createService();
store = spectator.service;
store.reset(createMockState());
});
describe('#getObjectExtensionEntitiesFromStore', () => {
it('should return observable entities of an existing module', async () => {
const entities = await getObjectExtensionEntitiesFromStore(store, 'Identity').toPromise();
expect('Role' in entities).toBe(true);
});
it('should return observable empty object if module does not exist', async () => {
const entities = await getObjectExtensionEntitiesFromStore(store, 'Saas').toPromise();
expect(entities).toEqual({});
});
it('should not emit when object extensions do not exist', done => {
store.reset({ ConfigState: {} });
const emit = jest.fn();
getObjectExtensionEntitiesFromStore(store, 'Identity').subscribe(emit);
setTimeout(() => {
expect(emit).not.toHaveBeenCalled();
done();
}, 1000);
});
});
describe('#mapEntitiesToContributors', () => {
it('should return contributors from given entities', async () => {
const contributors = await of(createMockEntities())
.pipe(mapEntitiesToContributors(store, 'AbpIdentity'), take(1))
.toPromise();
const propList = new EntityPropList();
contributors.prop.Role.forEach(callback => callback(propList));
expect(propList.length).toBe(3);
expect(propList.head.value.name).toBe('Title');
expect(propList.head.next.value.name).toBe('IsHero');
expect(propList.head.next.next.value.name).toBe('MyEnum');
const createFormList = new FormPropList();
contributors.createForm.Role.forEach(callback => callback(createFormList));
expect(createFormList.length).toBe(2);
expect(createFormList.head.value.name).toBe('Title');
expect(createFormList.head.next.value.name).toBe('MyEnum');
const editFormList = new FormPropList();
contributors.editForm.Role.forEach(callback => callback(editFormList));
expect(editFormList.length).toBe(2);
expect(editFormList.head.value.name).toBe('Title');
expect(editFormList.head.next.value.name).toBe('IsHero');
});
});
});
function createMockState(): MockState {
return {
ConfigState: {
objectExtensions: {
modules: {
Identity: {
entities: createMockEntities(),
configuration: null,
},
},
enums: {
'MyCompanyName.MyProjectName.MyEnum': {
fields: [
{
name: 'MyEnumValue0',
value: 0,
},
{
name: 'MyEnumValue1',
value: 1,
},
{
name: 'MyEnumValue2',
value: 2,
},
],
localizationResource: null,
},
},
},
localization: {
values: {
Default: {},
AbpIdentity: {},
},
defaultResourceName: 'Default',
currentCulture: null,
languages: [],
},
},
};
}
function createMockEntities(): ObjectExtensions.Entities {
return {
Role: {
properties: {
Title: {
type: 'System.String',
typeSimple: ePropType.String,
displayName: null,
api: {
onGet: {
isAvailable: true,
},
onCreate: {
isAvailable: true,
},
onUpdate: {
isAvailable: true,
},
},
ui: {
onTable: {
isSortable: true,
isVisible: true,
},
onCreateForm: {
isVisible: true,
},
onEditForm: {
isVisible: true,
},
},
attributes: [
{
typeSimple: 'required',
config: {},
},
{
typeSimple: 'stringLength',
config: {
maximumLength: 20,
minimumLength: 2,
},
},
],
configuration: {},
},
IsHero: {
type: 'System.Boolean',
typeSimple: ePropType.Boolean,
displayName: null,
api: {
onGet: {
isAvailable: true,
},
onCreate: {
isAvailable: true,
},
onUpdate: {
isAvailable: true,
},
},
ui: {
onTable: {
isSortable: false,
isVisible: true,
},
onCreateForm: {
isVisible: false,
},
onEditForm: {
isVisible: true,
},
},
attributes: [],
configuration: {},
},
AsOf: {
type: 'System.Date',
typeSimple: ePropType.Date,
displayName: {
name: 'Active as of',
resource: 'AbpIdentity',
},
api: {
onGet: {
isAvailable: true,
},
onCreate: {
isAvailable: true,
},
onUpdate: {
isAvailable: true,
},
},
ui: {
onTable: {
isSortable: false,
isVisible: false,
},
onCreateForm: {
isVisible: false,
},
onEditForm: {
isVisible: false,
},
},
attributes: [],
configuration: {},
},
MyEnum: {
type: 'MyCompanyName.MyProjectName.MyEnum',
typeSimple: ePropType.Enum,
displayName: null,
api: {
onGet: {
isAvailable: true,
},
onCreate: {
isAvailable: true,
},
onUpdate: {
isAvailable: true,
},
},
ui: {
onTable: {
isSortable: false,
isVisible: true,
},
onCreateForm: {
isVisible: true,
},
onEditForm: {
isVisible: false,
},
},
attributes: [
{
typeSimple: 'required',
config: {
allowEmptyStrings: false,
},
},
{
typeSimple: 'enumDataType',
config: {
enumType: 'MyCompanyName.MyProjectName.MyEnum',
dataType: 'Custom',
customDataType: 'Enumeration',
},
},
],
configuration: {},
defaultValue: 2,
},
},
configuration: {},
},
User: {
properties: null,
configuration: {},
},
ClaimType: null,
};
}
interface MockState {
ConfigState: {
localization: ApplicationConfiguration.Localization;
objectExtensions: ObjectExtensions.Item;
};
}

36
npm/ng-packs/packages/theme-shared/extensions/src/tests/time.adapter.spec.ts

@ -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);
});
});
});

216
npm/ng-packs/packages/theme-shared/extensions/src/tests/toolbar-actions.spec.ts

@ -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);
});
});
});

20
npm/ng-packs/packages/theme-shared/extensions/src/tests/validation.util.spec.ts

@ -0,0 +1,20 @@
import { Validators } from '@angular/forms';
import { ObjectExtensions } from '../lib/models/object-extensions';
import { getValidatorsFromProperty } from '../lib/utils/validation.util';
describe('Validation Utils', () => {
describe('#getValidatorsFromProperty', () => {
it('should return a list of validators derived from property attributes', () => {
const property = {
attributes: [
{
typeSimple: 'emailAddress',
config: {},
},
],
} as ObjectExtensions.Property;
expect(getValidatorsFromProperty(property)[0]).toBe(Validators.email);
});
});
});
Loading…
Cancel
Save