Browse Source

Merge pull request #6405 from abpframework/feat/3824

Added navigation property auto complete combobox for the extra properties
pull/6415/head
Bunyamin Coskuner 6 years ago
committed by GitHub
parent
commit
eb5897b786
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 53
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html
  2. 94
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts
  3. 4
      npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts
  4. 4
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts
  5. 2
      npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts
  6. 37
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/case.util.ts
  7. 24
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts
  8. 43
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/typeahead.util.ts
  9. 18
      npm/ng-packs/packages/theme-shared/extensions/src/tests/case.util.spec.ts
  10. 102
      npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts
  11. 72
      npm/ng-packs/packages/theme-shared/extensions/src/tests/typeahead.util.spec.ts
  12. 8
      npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts
  13. 93
      npm/ng-packs/yarn.lock

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

@ -1,6 +1,6 @@
<div class="form-group" *abpPermission="prop.permission" [ngSwitch]="getComponent(prop)"> <div class="form-group" *abpPermission="prop.permission" [ngSwitch]="getComponent(prop)">
<ng-template ngSwitchCase="input"> <ng-template ngSwitchCase="input">
<label [htmlFor]="prop.id">{{ prop.displayName | abpLocalization }} {{ asterisk }}</label> <ng-template [ngTemplateOutlet]="label"></ng-template>
<input <input
[id]="prop.id" [id]="prop.id"
[formControlName]="prop.name" [formControlName]="prop.name"
@ -12,6 +12,10 @@
/> />
</ng-template> </ng-template>
<ng-template ngSwitchCase="hidden">
<input [formControlName]="prop.name" type="hidden" />
</ng-template>
<ng-template ngSwitchCase="checkbox"> <ng-template ngSwitchCase="checkbox">
<div class="custom-checkbox custom-control" validationTarget> <div class="custom-checkbox custom-control" validationTarget>
<input <input
@ -21,14 +25,15 @@
type="checkbox" type="checkbox"
class="custom-control-input" class="custom-control-input"
/> />
<label [htmlFor]="prop.id" class="custom-control-label" <ng-template
>{{ prop.displayName | abpLocalization }} {{ asterisk }}</label [ngTemplateOutlet]="label"
> [ngTemplateOutletContext]="{ $implicit: 'custom-control-label' }"
></ng-template>
</div> </div>
</ng-template> </ng-template>
<ng-template ngSwitchCase="select"> <ng-template ngSwitchCase="select">
<label [htmlFor]="prop.id">{{ prop.displayName | abpLocalization }} {{ asterisk }}</label> <ng-template [ngTemplateOutlet]="label"></ng-template>
<select <select
[id]="prop.id" [id]="prop.id"
[formControlName]="prop.name" [formControlName]="prop.name"
@ -45,7 +50,7 @@
</ng-template> </ng-template>
<ng-template ngSwitchCase="multiselect"> <ng-template ngSwitchCase="multiselect">
<label [htmlFor]="prop.id">{{ prop.displayName | abpLocalization }} {{ asterisk }}</label> <ng-template [ngTemplateOutlet]="label"></ng-template>
<select <select
[id]="prop.id" [id]="prop.id"
[formControlName]="prop.name" [formControlName]="prop.name"
@ -62,8 +67,30 @@
</select> </select>
</ng-template> </ng-template>
<ng-template ngSwitchCase="typeahead">
<ng-template [ngTemplateOutlet]="label"></ng-template>
<div #typeahead class="position-relative" validationStyle validationTarget>
<input
[id]="prop.id"
[autocomplete]="prop.autocomplete"
[abpDisabled]="disabled"
[ngbTypeahead]="search"
[editable]="false"
[inputFormatter]="typeaheadFormatter"
[resultFormatter]="typeaheadFormatter"
[ngModelOptions]="{ standalone: true }"
[(ngModel)]="typeaheadModel"
(selectItem)="setTypeaheadValue($event.item)"
(blur)="setTypeaheadValue(typeaheadModel)"
[class.is-invalid]="typeahead.classList.contains('is-invalid')"
class="form-control"
/>
<input [formControlName]="prop.name" type="hidden" />
</div>
</ng-template>
<ng-template ngSwitchCase="date"> <ng-template ngSwitchCase="date">
<label [htmlFor]="prop.id">{{ prop.displayName | abpLocalization }} {{ asterisk }}</label> <ng-template [ngTemplateOutlet]="label"></ng-template>
<input <input
[id]="prop.id" [id]="prop.id"
[formControlName]="prop.name" [formControlName]="prop.name"
@ -77,17 +104,17 @@
</ng-template> </ng-template>
<ng-template ngSwitchCase="time"> <ng-template ngSwitchCase="time">
<label [htmlFor]="prop.id">{{ prop.displayName | abpLocalization }} {{ asterisk }}</label> <ng-template [ngTemplateOutlet]="label"></ng-template>
<ngb-timepicker [formControlName]="prop.name"></ngb-timepicker> <ngb-timepicker [formControlName]="prop.name"></ngb-timepicker>
</ng-template> </ng-template>
<ng-template ngSwitchCase="dateTime"> <ng-template ngSwitchCase="dateTime">
<label [htmlFor]="prop.id">{{ prop.displayName | abpLocalization }} {{ asterisk }}</label> <ng-template [ngTemplateOutlet]="label"></ng-template>
<abp-date-time-picker [prop]="prop"></abp-date-time-picker> <abp-date-time-picker [prop]="prop"></abp-date-time-picker>
</ng-template> </ng-template>
<ng-template ngSwitchCase="textarea"> <ng-template ngSwitchCase="textarea">
<label [htmlFor]="prop.id">{{ prop.displayName | abpLocalization }} {{ asterisk }}</label> <ng-template [ngTemplateOutlet]="label"></ng-template>
<textarea <textarea
[id]="prop.id" [id]="prop.id"
[formControlName]="prop.name" [formControlName]="prop.name"
@ -97,3 +124,9 @@
></textarea> ></textarea>
</ng-template> </ng-template>
</div> </div>
<ng-template #label let-classes>
<label [htmlFor]="prop.id" [ngClass]="classes"
>{{ prop.displayName | abpLocalization }} {{ asterisk }}</label
>
</ng-template>

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

@ -4,16 +4,25 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
Input, Input,
OnChanges,
Optional, Optional,
SimpleChanges, SimpleChanges,
SkipSelf, SkipSelf,
OnChanges,
} from '@angular/core'; } from '@angular/core';
import { ControlContainer, Validators, ValidatorFn } from '@angular/forms'; import {
ControlContainer,
FormGroup,
FormGroupDirective,
ValidatorFn,
Validators,
} from '@angular/forms';
import { NgbDateAdapter, NgbTimeAdapter } from '@ng-bootstrap/ng-bootstrap'; import { NgbDateAdapter, NgbTimeAdapter } from '@ng-bootstrap/ng-bootstrap';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import snq from 'snq';
import { DateAdapter } from '../../adapters/date.adapter'; import { DateAdapter } from '../../adapters/date.adapter';
import { TimeAdapter } from '../../adapters/time.adapter'; import { TimeAdapter } from '../../adapters/time.adapter';
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties';
import { ePropType } from '../../enums/props.enum'; import { ePropType } from '../../enums/props.enum';
import { FormProp } from '../../models/form-props'; import { FormProp } from '../../models/form-props';
import { PropData } from '../../models/props'; import { PropData } from '../../models/props';
@ -38,6 +47,8 @@ export class ExtensibleFormPropComponent implements OnChanges {
@Input() prop: FormProp; @Input() prop: FormProp;
asterisk = '';
options$: Observable<ABP.Option<any>[]> = of([]); options$: Observable<ABP.Option<any>[]> = of([]);
validators: ValidatorFn[] = []; validators: ValidatorFn[] = [];
@ -46,15 +57,59 @@ export class ExtensibleFormPropComponent implements OnChanges {
disabled: boolean; disabled: boolean;
constructor(public readonly cdRef: ChangeDetectorRef, public readonly track: TrackByService) {} private readonly form: FormGroup;
typeaheadModel: any;
get asterisk(): string { setTypeaheadValue(selectedOption: ABP.Option<string>) {
return this.validators.some(validator => validator === Validators.required) ? '*' : ''; this.typeaheadModel = selectedOption || { key: null, value: null };
const { key, value } = this.typeaheadModel;
const [keyControl, valueControl] = this.getTypeaheadControls();
keyControl.setValue(key);
valueControl.setValue(value);
valueControl.markAsDirty();
valueControl.markAsTouched();
} }
getComponent(prop: FormProp): string { search = (text$: Observable<string>) =>
if (prop.options && prop.type !== ePropType.MultiSelect) return 'select'; text$
? text$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(text => this.prop.options(this.data, text)),
)
: of([]);
typeaheadFormatter = (option: ABP.Option<any>) => option.key;
get isInvalid() {
const control = this.form.get(this.prop.name);
return control.touched && control.invalid;
}
constructor(
public readonly cdRef: ChangeDetectorRef,
public readonly track: TrackByService,
groupDirective: FormGroupDirective,
) {
this.form = groupDirective.form;
}
private getTypeaheadControls() {
const { name } = this.prop;
const textSuffix = '_Text';
const extraPropName = `${EXTRA_PROPERTIES_KEY}.${name}`;
const keyControl =
this.form.get(extraPropName + textSuffix) || this.form.get(name + textSuffix);
const valueControl = this.form.get(extraPropName) || this.form.get(name);
return [keyControl, valueControl];
}
private setAsterisk() {
this.asterisk = this.validators.some(v => v === Validators.required) ? '*' : '';
}
getComponent(prop: FormProp): string {
switch (prop.type) { switch (prop.type) {
case ePropType.Boolean: case ePropType.Boolean:
return 'checkbox'; return 'checkbox';
@ -62,14 +117,18 @@ export class ExtensibleFormPropComponent implements OnChanges {
return 'date'; return 'date';
case ePropType.DateTime: case ePropType.DateTime:
return 'dateTime'; return 'dateTime';
case ePropType.Hidden:
return 'hidden';
case ePropType.MultiSelect:
return 'multiselect';
case ePropType.Text: case ePropType.Text:
return 'textarea'; return 'textarea';
case ePropType.Time: case ePropType.Time:
return 'time'; return 'time';
case ePropType.MultiSelect: case ePropType.Typeahead:
return 'multiselect'; return 'typeahead';
default: default:
return 'input'; return prop.options ? 'select' : 'input';
} }
} }
@ -92,14 +151,19 @@ export class ExtensibleFormPropComponent implements OnChanges {
} }
ngOnChanges({ prop }: SimpleChanges) { ngOnChanges({ prop }: SimpleChanges) {
const options = prop.currentValue.options; const currentProp = snq<FormProp>(() => prop.currentValue);
const readonly = prop.currentValue.readonly; const { options, readonly, disabled, validators } = currentProp || {};
const disabled = prop.currentValue.disabled;
const validators = prop.currentValue.validators;
if (options) this.options$ = options(this.data); if (options) this.options$ = options(this.data);
if (readonly) this.readonly = readonly(this.data); if (readonly) this.readonly = readonly(this.data);
if (disabled) this.disabled = disabled(this.data); if (disabled) this.disabled = disabled(this.data);
if (validators) this.validators = validators(this.data); if (validators) {
this.validators = validators(this.data);
this.setAsterisk();
}
const [keyControl, valueControl] = this.getTypeaheadControls();
if (keyControl && valueControl)
this.typeaheadModel = { key: keyControl.value, value: valueControl.value };
} }
} }

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

@ -4,10 +4,12 @@ export const enum ePropType {
DateTime = 'dateTime', DateTime = 'dateTime',
Email = 'email', Email = 'email',
Enum = 'enum', Enum = 'enum',
Hidden = 'hidden',
MultiSelect = 'multiselect',
Number = 'number', Number = 'number',
Password = 'password', Password = 'password',
String = 'string', String = 'string',
Text = 'text', Text = 'text',
Time = 'time', Time = 'time',
MultiSelect = 'multiselect', Typeahead = 'typeahead',
} }

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

@ -39,8 +39,8 @@ export abstract class Prop<R = any> {
} }
} }
export type PropCallback<T, R = any> = (data?: Omit<PropData<T>, 'data'>) => R; export type PropCallback<T, R = any> = (data?: Omit<PropData<T>, 'data'>, auxData?: any) => R;
export type PropPredicate<T> = (data?: Omit<PropData<T>, 'data'>) => boolean; export type PropPredicate<T> = (data?: Omit<PropData<T>, 'data'>, auxData?: any) => boolean;
export abstract class PropsFactory<C extends Props<any>> { export abstract class PropsFactory<C extends Props<any>> {
protected abstract _ctor: Type<C>; protected abstract _ctor: Type<C>;

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

@ -5,6 +5,7 @@ import {
NgbDatepickerModule, NgbDatepickerModule,
NgbDropdownModule, NgbDropdownModule,
NgbTimepickerModule, NgbTimepickerModule,
NgbTypeaheadModule,
} from '@ng-bootstrap/ng-bootstrap'; } from '@ng-bootstrap/ng-bootstrap';
import { NgxValidateCoreModule } from '@ngx-validate/core'; import { NgxValidateCoreModule } from '@ngx-validate/core';
import { DateTimePickerComponent } from './components/date-time-picker/date-time-picker.component'; import { DateTimePickerComponent } from './components/date-time-picker/date-time-picker.component';
@ -43,6 +44,7 @@ import { PropDataDirective } from './directives/prop-data.directive';
NgbDatepickerModule, NgbDatepickerModule,
NgbDropdownModule, NgbDropdownModule,
NgbTimepickerModule, NgbTimepickerModule,
NgbTypeaheadModule,
], ],
}) })
export class UiExtensionsModule {} export class UiExtensionsModule {}

37
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/case.util.ts

@ -0,0 +1,37 @@
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/StringUtils.cs#L155
export function jsonNetCamelCase(str: string) {
if (!str || !isUpperCase(str[0])) return str;
const chars = str.split('');
const { length } = chars;
for (let i = 0; i < length; i++) {
if (i === 1 && !isUpperCase(chars[i])) break;
const hasNext = i + 1 < length;
if (i > 0 && hasNext && !isUpperCase(chars[i + 1])) {
if (isSeparator(chars[i + 1])) {
chars[i] = toLowerCase(chars[i]);
}
break;
}
chars[i] = toLowerCase(chars[i]);
}
return chars.join('');
}
function isSeparator(str = '') {
return /[\s\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-.\/:;<=>?@\[\]^_`{|}~]+/.test(str);
}
function isUpperCase(str = '') {
return /[A-Z]+/.test(str);
}
function toLowerCase(str = '') {
return str.toLowerCase();
}

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

@ -1,4 +1,9 @@
import { ABP, ApplicationLocalizationConfigurationDto, ConfigStateService } from '@abp/ng.core'; import {
ABP,
ApplicationLocalizationConfigurationDto,
ConfigStateService,
ExtensionPropertyUiLookupDto,
} from '@abp/ng.core';
import { Observable, pipe, zip } from 'rxjs'; import { Observable, pipe, zip } from 'rxjs';
import { filter, map, switchMap, take } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { ePropType } from '../enums/props.enum'; import { ePropType } from '../enums/props.enum';
@ -6,9 +11,11 @@ import { EntityProp, EntityPropList } from '../models/entity-props';
import { FormProp, FormPropList } from '../models/form-props'; import { FormProp, FormPropList } from '../models/form-props';
import { ObjectExtensions } from '../models/object-extensions'; import { ObjectExtensions } from '../models/object-extensions';
import { PropCallback } from '../models/props'; import { PropCallback } from '../models/props';
import { jsonNetCamelCase } from './case.util';
import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util'; import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util';
import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util'; import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util';
import { createExtraPropertyValueResolver } from './props.util'; import { createExtraPropertyValueResolver } from './props.util';
import { createTypeaheadOptions, getTypeaheadType } from './typeahead.util';
import { getValidatorsFromProperty } from './validation.util'; import { getValidatorsFromProperty } from './validation.util';
function selectObjectExtensions( function selectObjectExtensions(
@ -115,7 +122,9 @@ function createPropertiesToContributorsMapper<T = any>(
Object.keys(properties).forEach((name: string) => { Object.keys(properties).forEach((name: string) => {
const property = properties[name]; const property = properties[name];
const type = getTypeFromProperty(property); const propName = jsonNetCamelCase(name);
const lookup = property.ui.lookup || ({} as ExtensionPropertyUiLookupDto);
const type = getTypeaheadType(lookup, name) || getTypeFromProperty(property);
const displayName = generateDisplayName(property.displayName, { name, resource }); const displayName = generateDisplayName(property.displayName, { name, resource });
if (property.ui.onTable.isVisible) { if (property.ui.onTable.isVisible) {
@ -123,12 +132,12 @@ function createPropertiesToContributorsMapper<T = any>(
const columnWidth = type === ePropType.Boolean ? 150 : 250; const columnWidth = type === ePropType.Boolean ? 150 : 250;
const valueResolver = const valueResolver =
type === ePropType.Enum type === ePropType.Enum
? createEnumValueResolver(property.type, enums[property.type], name) ? createEnumValueResolver(property.type, enums[property.type], propName)
: createExtraPropertyValueResolver<T>(name); : createExtraPropertyValueResolver<T>(propName);
const entityProp = new EntityProp<T>({ const entityProp = new EntityProp<T>({
type, type,
name, name: propName,
displayName, displayName,
sortable, sortable,
columnWidth, columnWidth,
@ -147,11 +156,12 @@ function createPropertiesToContributorsMapper<T = any>(
const defaultValue = property.defaultValue; const defaultValue = property.defaultValue;
const validators = () => getValidatorsFromProperty(property); const validators = () => getValidatorsFromProperty(property);
let options: PropCallback<any, Observable<ABP.Option<any>[]>>; let options: PropCallback<any, Observable<ABP.Option<any>[]>>;
if (type === ePropType.Enum) options = createEnumOptions(name, enums[property.type]); if (type === ePropType.Enum) options = createEnumOptions(propName, enums[property.type]);
else if (type === ePropType.Typeahead) options = createTypeaheadOptions(lookup);
const formProp = new FormProp({ const formProp = new FormProp({
type, type,
name, name: propName,
displayName, displayName,
options, options,
defaultValue, defaultValue,

43
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/typeahead.util.ts

@ -0,0 +1,43 @@
import { ABP, ExtensionPropertyUiLookupDto, RestService } from '@abp/ng.core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { ePropType } from '../enums/props.enum';
import { PropCallback } from '../models/props';
export function createTypeaheadOptions(
lookup: ExtensionPropertyUiLookupDto,
): PropCallback<any, Observable<ABP.Option<any>[]>> {
return (data, searchText) =>
searchText
? data
.getInjected(RestService)
.request(
{
method: 'GET',
url: lookup.url,
params: {
[lookup.filterParamName]: searchText,
},
},
{ apiName: 'Default' },
)
.pipe(
map(response => {
const list = response[lookup.resultListPropertyName];
const mapToOption = (item: any) => ({
key: item[lookup.displayPropertyName],
value: item[lookup.valuePropertyName],
});
return list.map(mapToOption);
}),
)
: of([]);
}
export function getTypeaheadType(lookup: ExtensionPropertyUiLookupDto, name: string) {
return Boolean(lookup.url)
? ePropType.Typeahead
: name.endsWith('_Text')
? ePropType.Hidden
: undefined;
}

18
npm/ng-packs/packages/theme-shared/extensions/src/tests/case.util.spec.ts

@ -0,0 +1,18 @@
import { jsonNetCamelCase } from '../lib/utils/case.util';
describe('Case Utils', () => {
describe('#jsonNetCamelCase', () => {
test.each`
input | output
${'Primary'} | ${'primary'}
${'PrimaryRole'} | ${'primaryRole'}
${'Primary Role'} | ${'primary Role'}
${'PrimaryRole_Text'} | ${'primaryRole_Text'}
${'ISBN'} | ${'isbn'}
${''} | ${''}
${'iMDB'} | ${'iMDB'}
`('should return $output when input is $input', ({ input, output }) => {
expect(jsonNetCamelCase(input)).toBe(output);
});
});
});

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

@ -50,24 +50,29 @@ describe('State Utils', () => {
const propList = new EntityPropList(); const propList = new EntityPropList();
contributors.prop.Role.forEach(callback => callback(propList)); contributors.prop.Role.forEach(callback => callback(propList));
expect(propList.length).toBe(3); expect(propList.length).toBe(4);
expect(propList.head.value.name).toBe('Title'); expect(propList.head.value.name).toBe('title');
expect(propList.head.next.value.name).toBe('IsHero'); expect(propList.head.next.value.name).toBe('isHero');
expect(propList.head.next.next.value.name).toBe('MyEnum'); expect(propList.head.next.next.value.name).toBe('myEnum');
expect(propList.head.next.next.next.value.name).toBe('foo_Text');
const createFormList = new FormPropList(); const createFormList = new FormPropList();
contributors.createForm.Role.forEach(callback => callback(createFormList)); contributors.createForm.Role.forEach(callback => callback(createFormList));
expect(createFormList.length).toBe(2); expect(createFormList.length).toBe(4);
expect(createFormList.head.value.name).toBe('Title'); expect(createFormList.head.value.name).toBe('title');
expect(createFormList.head.next.value.name).toBe('MyEnum'); expect(createFormList.head.next.value.name).toBe('myEnum');
expect(createFormList.head.next.next.value.name).toBe('foo');
expect(createFormList.head.next.next.next.value.name).toBe('foo_Text');
const editFormList = new FormPropList(); const editFormList = new FormPropList();
contributors.editForm.Role.forEach(callback => callback(editFormList)); contributors.editForm.Role.forEach(callback => callback(editFormList));
expect(editFormList.length).toBe(2); expect(editFormList.length).toBe(4);
expect(editFormList.head.value.name).toBe('Title'); expect(editFormList.head.value.name).toBe('title');
expect(editFormList.head.next.value.name).toBe('IsHero'); expect(editFormList.head.next.value.name).toBe('isHero');
expect(editFormList.head.next.next.value.name).toBe('foo');
expect(editFormList.head.next.next.next.value.name).toBe('foo_Text');
}); });
}); });
}); });
@ -275,6 +280,83 @@ function createMockEntities(): Record<string, ObjectExtensions.EntityExtensionDt
configuration: {}, configuration: {},
defaultValue: 2, defaultValue: 2,
}, },
Foo: {
type: 'System.String',
typeSimple: ePropType.String,
displayName: null,
api: {
onGet: {
isAvailable: false,
},
onCreate: {
isAvailable: true,
},
onUpdate: {
isAvailable: true,
},
},
ui: {
onTable: {
isVisible: false,
},
onCreateForm: {
isVisible: true,
},
onEditForm: {
isVisible: true,
},
lookup: {
url: '/api/identity/roles',
resultListPropertyName: 'items',
displayPropertyName: 'text',
valuePropertyName: 'id',
filterParamName: 'filter',
},
},
attributes: [],
configuration: {},
defaultValue: null,
},
Foo_Text: {
type: 'System.String',
typeSimple: ePropType.String,
displayName: {
name: 'Foo',
resource: '_',
},
api: {
onGet: {
isAvailable: true,
},
onCreate: {
isAvailable: true,
},
onUpdate: {
isAvailable: true,
},
},
ui: {
onTable: {
isVisible: true,
},
onCreateForm: {
isVisible: true,
},
onEditForm: {
isVisible: true,
},
lookup: {
url: null,
resultListPropertyName: 'items',
displayPropertyName: 'text',
valuePropertyName: 'id',
filterParamName: 'filter',
},
},
attributes: [],
configuration: {},
defaultValue: null,
},
}, },
configuration: {}, configuration: {},
}, },

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

@ -0,0 +1,72 @@
import { ExtensionPropertyUiLookupDto } from '@abp/ng.core';
import { of } from 'rxjs';
import { createTypeaheadOptions } from '../lib/utils/typeahead.util';
const lookup: ExtensionPropertyUiLookupDto = {
url: 'url',
resultListPropertyName: 'list',
displayPropertyName: 'text',
valuePropertyName: 'id',
filterParamName: 'filter',
};
describe('Typeahead Utils', () => {
describe('#createTypeaheadOptions', () => {
it('should return observable empty array when search text does not exist', async () => {
const list = await createTypeaheadOptions(null)(null, null).toPromise();
expect(list).toEqual([]);
});
it('should call request method of RestService with lookup url, filter param and search text', async () => {
const data = createData([]);
const service = data.getInjected();
await createTypeaheadOptions(lookup)(data, 'x').toPromise();
expect(service.request).toHaveBeenCalledTimes(1);
expect(service.request).toHaveBeenCalledWith(
{
method: 'GET',
url: 'url',
params: {
filter: 'x',
},
},
{ apiName: 'Default' },
);
});
it('should return options based on given lookup data', async () => {
const data = createData([
{
text: 'foo',
id: 'bar',
},
{
text: 'baz',
id: 'qux',
},
]);
const options = await createTypeaheadOptions(lookup)(data, 'x').toPromise();
expect(options).toEqual([
{
key: 'foo',
value: 'bar',
},
{
key: 'baz',
value: 'qux',
},
]);
});
});
});
function createData(list: { text: string; id: string }[]): any {
const service = { request: jest.fn(() => of({ list })) };
return {
getInjected: () => service,
index: 0,
record: null,
};
}

8
npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts

@ -2,6 +2,11 @@ export default `
.is-invalid .form-control { .is-invalid .form-control {
border-color: #dc3545; border-color: #dc3545;
border-style: solid !important; border-style: solid !important;
padding-right: calc(1.5em + .75rem);
background-image: url(data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e);
background-repeat: no-repeat;
background-position: right calc(.375em + .1875rem) center;
background-size: calc(.75em + .375rem) calc(.75em + .375rem);
} }
.is-invalid .invalid-feedback, .is-invalid .invalid-feedback,
@ -144,6 +149,9 @@ export default `
.ngx-datatable.material { .ngx-datatable.material {
box-shadow: none; box-shadow: none;
} }
ngb-typeahead-window {
width: 100%;
}
@keyframes fadeInTop { @keyframes fadeInTop {
from { from {

93
npm/ng-packs/yarn.lock

@ -2,12 +2,12 @@
# yarn lockfile v1 # yarn lockfile v1
"@abp/ng.core@~4.0.0-rc.3": "@abp/ng.core@~4.0.0-rc.4":
version "4.0.0-rc.3" version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-4.0.0-rc.3.tgz#fe85e2e7b727f88829e0242674b33336be7de2b3" resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-4.0.0-rc.4.tgz#f1cbb7d43662579a7977c9d98bf28f7a6726537f"
integrity sha512-di4gGOb4y/G2vden4VMSTkRAC4J03w8qvFA2Wwmz4n7TSl3p5fP8/jnewJWJskaVJyuEiDk6EohEAkFmZd+rdg== integrity sha512-EtvoUr52v4+DEn3j2mru4OiLJf9tcg5ptLRjwBg1psftz1t3atnmvP1F8+sIhOwnj/Zm5hlhqSqnedpTzdALPg==
dependencies: dependencies:
"@abp/utils" "^4.0.0-rc.2" "@abp/utils" "^4.0.0-rc.3"
"@angular/localize" "~10.0.10" "@angular/localize" "~10.0.10"
"@ngxs/router-plugin" "^3.7.0" "@ngxs/router-plugin" "^3.7.0"
"@ngxs/store" "^3.7.0" "@ngxs/store" "^3.7.0"
@ -18,29 +18,29 @@
ts-toolbelt "6.15.4" ts-toolbelt "6.15.4"
tslib "^2.0.0" tslib "^2.0.0"
"@abp/ng.feature-management@~4.0.0-rc.3": "@abp/ng.feature-management@~4.0.0-rc.4":
version "4.0.0-rc.3" version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-4.0.0-rc.3.tgz#cb8dbb03f4a51653d09959c34b11a6abccc9d0c4" resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-4.0.0-rc.4.tgz#ab740e4327b8fd10ba4567bfeb24debbd4ffe8a5"
integrity sha512-Ftmk6muh09IuIL3BadYHi6J75LANjTdCcGEjsXsYU1Gvv4cWffkRlrJmrVRdfQxW1XqFDdBV1+AB02g8NA3llw== integrity sha512-PlWxAA2wECRrBGwqWeaWi+f3YPayIaOJhW4INTvD5JUFMIRJsOJZlxpmcz8x2pU64bUO42/YM26+Xy2TyVKFNg==
dependencies: dependencies:
"@abp/ng.theme.shared" "~4.0.0-rc.3" "@abp/ng.theme.shared" "~4.0.0-rc.4"
tslib "^2.0.0" tslib "^2.0.0"
"@abp/ng.identity@~4.0.0-rc.3": "@abp/ng.identity@~4.0.0-rc.4":
version "4.0.0-rc.3" version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/ng.identity/-/ng.identity-4.0.0-rc.3.tgz#0a8aba1ea0d4c7e4f59407bc4f6eb210250a450b" resolved "https://registry.yarnpkg.com/@abp/ng.identity/-/ng.identity-4.0.0-rc.4.tgz#5b8694b1d1d6f1a039e1a445add14dcc5d6eb5ad"
integrity sha512-nXXAdsbJsvc06/b5T5rR4nbApxInaK9fL9ItLCoSzPHB9vUl5FTZw+oQYg4cnZhIhouThrAoAVpr2CxRxmeApg== integrity sha512-nqx3ofuTT5kOdJHWAyrZyEmGGp08DZNNC7IIY7L1/NueiAcfjghKrAiGkNnt5SApdk86OzdrLdIvpELnZVCMzg==
dependencies: dependencies:
"@abp/ng.permission-management" "~4.0.0-rc.3" "@abp/ng.permission-management" "~4.0.0-rc.4"
"@abp/ng.theme.shared" "~4.0.0-rc.3" "@abp/ng.theme.shared" "~4.0.0-rc.4"
tslib "^2.0.0" tslib "^2.0.0"
"@abp/ng.permission-management@~4.0.0-rc.3": "@abp/ng.permission-management@~4.0.0-rc.4":
version "4.0.0-rc.3" version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-4.0.0-rc.3.tgz#92fb7a03b91d10816661d64126817375d33abf46" resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-4.0.0-rc.4.tgz#3d838528549860e2a8b6aba2c5f35f6764089322"
integrity sha512-NdOyytrzmIoewZlY3FeO1P7EQGzHySBgCUXFSmUstBmx5L8TWFRTsKmUZoXnRlO8bzLlSElpfgVmi4uEgLzKqg== integrity sha512-2o/1dtfNQOkqHWzdHyI8Ske5E5VseCM+N3erJu4NSLIWrj5G3CG9wwbJm3BqEjHG7hzbcvR6fYxKpDUmqvQR0A==
dependencies: dependencies:
"@abp/ng.theme.shared" "~4.0.0-rc.3" "@abp/ng.theme.shared" "~4.0.0-rc.4"
tslib "^2.0.0" tslib "^2.0.0"
"@abp/ng.schematics@~3.3.1": "@abp/ng.schematics@~3.3.1":
@ -54,37 +54,37 @@
jsonc-parser "^2.3.0" jsonc-parser "^2.3.0"
typescript "~3.9.2" typescript "~3.9.2"
"@abp/ng.setting-management@~4.0.0-rc.3": "@abp/ng.setting-management@~4.0.0-rc.4":
version "4.0.0-rc.3" version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/ng.setting-management/-/ng.setting-management-4.0.0-rc.3.tgz#931227b7b81bd7e68ff0ad0db902269534df3bf4" resolved "https://registry.yarnpkg.com/@abp/ng.setting-management/-/ng.setting-management-4.0.0-rc.4.tgz#45e7fe04a8069da19d644860617e5b5aeab39281"
integrity sha512-sdC4U05xiPAkqzweivz0MBF0ZEZBjFIijAIjA6s+1VReF49RwIKH8R+1nzf5XO0FZmwkWYza7TIxpJwuocZRZg== integrity sha512-L7liYuB+DdWdcdJwG4OkQhBdb1bNqFO08TjfbRdkPARHje/tat+z8jtZ58yQVYOVCs9M2vJt3ydHnvZ+PQWpow==
dependencies: dependencies:
"@abp/ng.theme.shared" "~4.0.0-rc.3" "@abp/ng.theme.shared" "~4.0.0-rc.4"
tslib "^2.0.0" tslib "^2.0.0"
"@abp/ng.tenant-management@~4.0.0-rc.3": "@abp/ng.tenant-management@~4.0.0-rc.4":
version "4.0.0-rc.3" version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management/-/ng.tenant-management-4.0.0-rc.3.tgz#ee40e557f648eab2f9a7d0da2984c190878ca828" resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management/-/ng.tenant-management-4.0.0-rc.4.tgz#0978a65423b9f56df1fd58eaaa01fd912b2875ab"
integrity sha512-0UaGKS2htCA2vjZcrxMQk4oBHcQBoeAZgVZ93ehEZkhN5gW1JKlOZpzNX7uDtbbFYshD4srlBBrdhoYS4ENSCw== integrity sha512-K7BCZMTs/nPoPrDo/yGu3IWZjXktZHcq0qg2pvjVZRqlfbgk0m+mih01H6WC+/tjKYfdb4PtWQJ36mGR17zvuQ==
dependencies: dependencies:
"@abp/ng.feature-management" "~4.0.0-rc.3" "@abp/ng.feature-management" "~4.0.0-rc.4"
"@abp/ng.theme.shared" "~4.0.0-rc.3" "@abp/ng.theme.shared" "~4.0.0-rc.4"
tslib "^2.0.0" tslib "^2.0.0"
"@abp/ng.theme.basic@~4.0.0-rc.3": "@abp/ng.theme.basic@~4.0.0-rc.4":
version "4.0.0-rc.3" version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.basic/-/ng.theme.basic-4.0.0-rc.3.tgz#2f1d4ab66a6e7208a03086fe0cffcb86df0dfdf3" resolved "https://registry.yarnpkg.com/@abp/ng.theme.basic/-/ng.theme.basic-4.0.0-rc.4.tgz#438d73fed9962dae3d92b0d1f87f36e3601ab720"
integrity sha512-C/mWx3UeXiohVCYC5cLEJJhqr0d/3q+udwUObiO668HUaJogHi76oseoVeCEWzGs3ldVus4kMQvtYSummZtV6g== integrity sha512-Gunzo6N1YoGZ3crLCYqflmvnuayekyV5Aa63dwCikOTloEBmfQy4FylAcUvncNo841BwmWOk7oDGmxQmrx+5bw==
dependencies: dependencies:
"@abp/ng.theme.shared" "~4.0.0-rc.3" "@abp/ng.theme.shared" "~4.0.0-rc.4"
tslib "^2.0.0" tslib "^2.0.0"
"@abp/ng.theme.shared@~4.0.0-rc.3": "@abp/ng.theme.shared@~4.0.0-rc.4":
version "4.0.0-rc.3" version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-4.0.0-rc.3.tgz#aa4cf7516b2ecacd742916fdfb77019eae327ece" resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-4.0.0-rc.4.tgz#6341acbb647c8606c1147cad4387334a42908a2c"
integrity sha512-z44kFjoI2ngunrnR58KNsWpliekt4K9FNino/ccoI6WpXoPGAsop9vz4x0T6my+Q56mV7hb08h7C6GTqBDEK5g== integrity sha512-r3V+PIJC9pCA1OD7xL6I+sJ0AFeF/wcjyUJvn7ZRvevK+eDkHf6AqTXRCNgd+VTfCqtjIpiU5oC7rSOB1gGwPw==
dependencies: dependencies:
"@abp/ng.core" "~4.0.0-rc.3" "@abp/ng.core" "~4.0.0-rc.4"
"@fortawesome/fontawesome-free" "^5.14.0" "@fortawesome/fontawesome-free" "^5.14.0"
"@ng-bootstrap/ng-bootstrap" "^7.0.0" "@ng-bootstrap/ng-bootstrap" "^7.0.0"
"@ngx-validate/core" "^0.0.12" "@ngx-validate/core" "^0.0.12"
@ -93,13 +93,20 @@
chart.js "^2.9.3" chart.js "^2.9.3"
tslib "^2.0.0" tslib "^2.0.0"
"@abp/utils@^4.0.0-rc.2", "@abp/utils@^4.0.0-rc.3": "@abp/utils@^4.0.0-rc.3":
version "4.0.0-rc.3" version "4.0.0-rc.3"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-4.0.0-rc.3.tgz#95a67fc0f7c929e0404d33ee2ab8571c30e7f28d" resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-4.0.0-rc.3.tgz#95a67fc0f7c929e0404d33ee2ab8571c30e7f28d"
integrity sha512-6KH7Tddx5vQQ8j3vhDlveAyDw8VSnLlTpEKaNtQmIyJ+yZ025Faa3m+MKb83jXxDE9hPi3v8QGagEOT5TQjNHg== integrity sha512-6KH7Tddx5vQQ8j3vhDlveAyDw8VSnLlTpEKaNtQmIyJ+yZ025Faa3m+MKb83jXxDE9hPi3v8QGagEOT5TQjNHg==
dependencies: dependencies:
just-compare "^1.3.0" just-compare "^1.3.0"
"@abp/utils@^4.0.0-rc.4":
version "4.0.0-rc.4"
resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-4.0.0-rc.4.tgz#99573069c7dac8751e3124e60511a11a169df565"
integrity sha512-z1l6A07+rSgf7fzI/UTlZv1cJBiIUa0kpEvhcChe7mwlHB7oQ3SlGjZX37kzOqWcOusfMOANPuVulkZLSCOB/w==
dependencies:
just-compare "^1.3.0"
"@angular-builders/jest@^10.0.0": "@angular-builders/jest@^10.0.0":
version "10.0.1" version "10.0.1"
resolved "https://registry.yarnpkg.com/@angular-builders/jest/-/jest-10.0.1.tgz#a1a6fb5d11b5d54c051bdaa2012b5f046371560c" resolved "https://registry.yarnpkg.com/@angular-builders/jest/-/jest-10.0.1.tgz#a1a6fb5d11b5d54c051bdaa2012b5f046371560c"

Loading…
Cancel
Save