From 772f7a3a111ebe416c46bb7050276e048a7c4ea8 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 00:44:53 +0300 Subject: [PATCH 01/12] chore: update yarn.lock --- npm/ng-packs/yarn.lock | 93 +++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/npm/ng-packs/yarn.lock b/npm/ng-packs/yarn.lock index d32e47137e..ca4a83fc40 100644 --- a/npm/ng-packs/yarn.lock +++ b/npm/ng-packs/yarn.lock @@ -2,12 +2,12 @@ # yarn lockfile v1 -"@abp/ng.core@~4.0.0-rc.3": - version "4.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-4.0.0-rc.3.tgz#fe85e2e7b727f88829e0242674b33336be7de2b3" - integrity sha512-di4gGOb4y/G2vden4VMSTkRAC4J03w8qvFA2Wwmz4n7TSl3p5fP8/jnewJWJskaVJyuEiDk6EohEAkFmZd+rdg== +"@abp/ng.core@~4.0.0-rc.4": + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/@abp/ng.core/-/ng.core-4.0.0-rc.4.tgz#f1cbb7d43662579a7977c9d98bf28f7a6726537f" + integrity sha512-EtvoUr52v4+DEn3j2mru4OiLJf9tcg5ptLRjwBg1psftz1t3atnmvP1F8+sIhOwnj/Zm5hlhqSqnedpTzdALPg== dependencies: - "@abp/utils" "^4.0.0-rc.2" + "@abp/utils" "^4.0.0-rc.3" "@angular/localize" "~10.0.10" "@ngxs/router-plugin" "^3.7.0" "@ngxs/store" "^3.7.0" @@ -18,29 +18,29 @@ ts-toolbelt "6.15.4" tslib "^2.0.0" -"@abp/ng.feature-management@~4.0.0-rc.3": - version "4.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-4.0.0-rc.3.tgz#cb8dbb03f4a51653d09959c34b11a6abccc9d0c4" - integrity sha512-Ftmk6muh09IuIL3BadYHi6J75LANjTdCcGEjsXsYU1Gvv4cWffkRlrJmrVRdfQxW1XqFDdBV1+AB02g8NA3llw== +"@abp/ng.feature-management@~4.0.0-rc.4": + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/@abp/ng.feature-management/-/ng.feature-management-4.0.0-rc.4.tgz#ab740e4327b8fd10ba4567bfeb24debbd4ffe8a5" + integrity sha512-PlWxAA2wECRrBGwqWeaWi+f3YPayIaOJhW4INTvD5JUFMIRJsOJZlxpmcz8x2pU64bUO42/YM26+Xy2TyVKFNg== dependencies: - "@abp/ng.theme.shared" "~4.0.0-rc.3" + "@abp/ng.theme.shared" "~4.0.0-rc.4" tslib "^2.0.0" -"@abp/ng.identity@~4.0.0-rc.3": - version "4.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@abp/ng.identity/-/ng.identity-4.0.0-rc.3.tgz#0a8aba1ea0d4c7e4f59407bc4f6eb210250a450b" - integrity sha512-nXXAdsbJsvc06/b5T5rR4nbApxInaK9fL9ItLCoSzPHB9vUl5FTZw+oQYg4cnZhIhouThrAoAVpr2CxRxmeApg== +"@abp/ng.identity@~4.0.0-rc.4": + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/@abp/ng.identity/-/ng.identity-4.0.0-rc.4.tgz#5b8694b1d1d6f1a039e1a445add14dcc5d6eb5ad" + integrity sha512-nqx3ofuTT5kOdJHWAyrZyEmGGp08DZNNC7IIY7L1/NueiAcfjghKrAiGkNnt5SApdk86OzdrLdIvpELnZVCMzg== dependencies: - "@abp/ng.permission-management" "~4.0.0-rc.3" - "@abp/ng.theme.shared" "~4.0.0-rc.3" + "@abp/ng.permission-management" "~4.0.0-rc.4" + "@abp/ng.theme.shared" "~4.0.0-rc.4" tslib "^2.0.0" -"@abp/ng.permission-management@~4.0.0-rc.3": - version "4.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-4.0.0-rc.3.tgz#92fb7a03b91d10816661d64126817375d33abf46" - integrity sha512-NdOyytrzmIoewZlY3FeO1P7EQGzHySBgCUXFSmUstBmx5L8TWFRTsKmUZoXnRlO8bzLlSElpfgVmi4uEgLzKqg== +"@abp/ng.permission-management@~4.0.0-rc.4": + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/@abp/ng.permission-management/-/ng.permission-management-4.0.0-rc.4.tgz#3d838528549860e2a8b6aba2c5f35f6764089322" + integrity sha512-2o/1dtfNQOkqHWzdHyI8Ske5E5VseCM+N3erJu4NSLIWrj5G3CG9wwbJm3BqEjHG7hzbcvR6fYxKpDUmqvQR0A== dependencies: - "@abp/ng.theme.shared" "~4.0.0-rc.3" + "@abp/ng.theme.shared" "~4.0.0-rc.4" tslib "^2.0.0" "@abp/ng.schematics@~3.3.1": @@ -54,37 +54,37 @@ jsonc-parser "^2.3.0" typescript "~3.9.2" -"@abp/ng.setting-management@~4.0.0-rc.3": - version "4.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@abp/ng.setting-management/-/ng.setting-management-4.0.0-rc.3.tgz#931227b7b81bd7e68ff0ad0db902269534df3bf4" - integrity sha512-sdC4U05xiPAkqzweivz0MBF0ZEZBjFIijAIjA6s+1VReF49RwIKH8R+1nzf5XO0FZmwkWYza7TIxpJwuocZRZg== +"@abp/ng.setting-management@~4.0.0-rc.4": + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/@abp/ng.setting-management/-/ng.setting-management-4.0.0-rc.4.tgz#45e7fe04a8069da19d644860617e5b5aeab39281" + integrity sha512-L7liYuB+DdWdcdJwG4OkQhBdb1bNqFO08TjfbRdkPARHje/tat+z8jtZ58yQVYOVCs9M2vJt3ydHnvZ+PQWpow== dependencies: - "@abp/ng.theme.shared" "~4.0.0-rc.3" + "@abp/ng.theme.shared" "~4.0.0-rc.4" tslib "^2.0.0" -"@abp/ng.tenant-management@~4.0.0-rc.3": - version "4.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management/-/ng.tenant-management-4.0.0-rc.3.tgz#ee40e557f648eab2f9a7d0da2984c190878ca828" - integrity sha512-0UaGKS2htCA2vjZcrxMQk4oBHcQBoeAZgVZ93ehEZkhN5gW1JKlOZpzNX7uDtbbFYshD4srlBBrdhoYS4ENSCw== +"@abp/ng.tenant-management@~4.0.0-rc.4": + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/@abp/ng.tenant-management/-/ng.tenant-management-4.0.0-rc.4.tgz#0978a65423b9f56df1fd58eaaa01fd912b2875ab" + integrity sha512-K7BCZMTs/nPoPrDo/yGu3IWZjXktZHcq0qg2pvjVZRqlfbgk0m+mih01H6WC+/tjKYfdb4PtWQJ36mGR17zvuQ== dependencies: - "@abp/ng.feature-management" "~4.0.0-rc.3" - "@abp/ng.theme.shared" "~4.0.0-rc.3" + "@abp/ng.feature-management" "~4.0.0-rc.4" + "@abp/ng.theme.shared" "~4.0.0-rc.4" tslib "^2.0.0" -"@abp/ng.theme.basic@~4.0.0-rc.3": - version "4.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@abp/ng.theme.basic/-/ng.theme.basic-4.0.0-rc.3.tgz#2f1d4ab66a6e7208a03086fe0cffcb86df0dfdf3" - integrity sha512-C/mWx3UeXiohVCYC5cLEJJhqr0d/3q+udwUObiO668HUaJogHi76oseoVeCEWzGs3ldVus4kMQvtYSummZtV6g== +"@abp/ng.theme.basic@~4.0.0-rc.4": + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/@abp/ng.theme.basic/-/ng.theme.basic-4.0.0-rc.4.tgz#438d73fed9962dae3d92b0d1f87f36e3601ab720" + integrity sha512-Gunzo6N1YoGZ3crLCYqflmvnuayekyV5Aa63dwCikOTloEBmfQy4FylAcUvncNo841BwmWOk7oDGmxQmrx+5bw== dependencies: - "@abp/ng.theme.shared" "~4.0.0-rc.3" + "@abp/ng.theme.shared" "~4.0.0-rc.4" tslib "^2.0.0" -"@abp/ng.theme.shared@~4.0.0-rc.3": - version "4.0.0-rc.3" - resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-4.0.0-rc.3.tgz#aa4cf7516b2ecacd742916fdfb77019eae327ece" - integrity sha512-z44kFjoI2ngunrnR58KNsWpliekt4K9FNino/ccoI6WpXoPGAsop9vz4x0T6my+Q56mV7hb08h7C6GTqBDEK5g== +"@abp/ng.theme.shared@~4.0.0-rc.4": + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/@abp/ng.theme.shared/-/ng.theme.shared-4.0.0-rc.4.tgz#6341acbb647c8606c1147cad4387334a42908a2c" + integrity sha512-r3V+PIJC9pCA1OD7xL6I+sJ0AFeF/wcjyUJvn7ZRvevK+eDkHf6AqTXRCNgd+VTfCqtjIpiU5oC7rSOB1gGwPw== dependencies: - "@abp/ng.core" "~4.0.0-rc.3" + "@abp/ng.core" "~4.0.0-rc.4" "@fortawesome/fontawesome-free" "^5.14.0" "@ng-bootstrap/ng-bootstrap" "^7.0.0" "@ngx-validate/core" "^0.0.12" @@ -93,13 +93,20 @@ chart.js "^2.9.3" 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" resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-4.0.0-rc.3.tgz#95a67fc0f7c929e0404d33ee2ab8571c30e7f28d" integrity sha512-6KH7Tddx5vQQ8j3vhDlveAyDw8VSnLlTpEKaNtQmIyJ+yZ025Faa3m+MKb83jXxDE9hPi3v8QGagEOT5TQjNHg== dependencies: 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": version "10.0.1" resolved "https://registry.yarnpkg.com/@angular-builders/jest/-/jest-10.0.1.tgz#a1a6fb5d11b5d54c051bdaa2012b5f046371560c" From 3aa38eb685235bf0088046b53589808cb8c12736 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 00:45:33 +0300 Subject: [PATCH 02/12] feat: import NgbTypeaheadModule in UiExtensionsModule --- .../theme-shared/extensions/src/lib/ui-extensions.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts index c0c93ce151..11f9ff9e4d 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts @@ -5,6 +5,7 @@ import { NgbDatepickerModule, NgbDropdownModule, NgbTimepickerModule, + NgbTypeaheadModule, } from '@ng-bootstrap/ng-bootstrap'; import { NgxValidateCoreModule } from '@ngx-validate/core'; import { DateTimePickerComponent } from './components/date-time-picker/date-time-picker.component'; @@ -43,6 +44,7 @@ import { PropDataDirective } from './directives/prop-data.directive'; NgbDatepickerModule, NgbDropdownModule, NgbTimepickerModule, + NgbTypeaheadModule, ], }) export class UiExtensionsModule {} From 4a8a996d76955f7501ba4ce11ddc8627ba8fdd17 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 00:46:26 +0300 Subject: [PATCH 03/12] feat: add auxData to PropCallback and PropPredicate types --- .../packages/theme-shared/extensions/src/lib/models/props.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts index 1e2fa9a0a0..ef37bee218 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts @@ -39,8 +39,8 @@ export abstract class Prop { } } -export type PropCallback = (data?: Omit, 'data'>) => R; -export type PropPredicate = (data?: Omit, 'data'>) => boolean; +export type PropCallback = (data?: Omit, 'data'>, auxData?: any) => R; +export type PropPredicate = (data?: Omit, 'data'>, auxData?: any) => boolean; export abstract class PropsFactory> { protected abstract _ctor: Type; From 52dac8057098242f559003096e0571cd1dcf1892 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 00:47:08 +0300 Subject: [PATCH 04/12] feat: add Hidden and Typeahead to prop types enum --- .../theme-shared/extensions/src/lib/enums/props.enum.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts index a7c989569f..27cb4990a6 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts @@ -4,10 +4,12 @@ export const enum ePropType { DateTime = 'dateTime', Email = 'email', Enum = 'enum', + Hidden = 'hidden', + MultiSelect = 'multiselect', Number = 'number', Password = 'password', String = 'string', Text = 'text', Time = 'time', - MultiSelect = 'multiselect', + Typeahead = 'typeahead', } From 311fe150315c434b35bdc16d1765da8de3af0ce0 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 00:47:53 +0300 Subject: [PATCH 05/12] feat: add typeahead to extensible form props --- .../extensible-form-prop.component.html | 53 +++++++++--- .../extensible-form-prop.component.ts | 85 +++++++++++++++---- 2 files changed, 113 insertions(+), 25 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html b/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html index 60f35d636e..f3f60a56dc 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html @@ -1,6 +1,6 @@
- + + + + +
- +
- + + + +
+ + +
+
+ - + - + - + - +
+ + + + diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts index 6b5b241924..60ae170957 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts @@ -4,14 +4,22 @@ import { ChangeDetectorRef, Component, Input, + OnChanges, Optional, SimpleChanges, SkipSelf, - OnChanges, } 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 { Observable, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; +import snq from 'snq'; import { DateAdapter } from '../../adapters/date.adapter'; import { TimeAdapter } from '../../adapters/time.adapter'; import { ePropType } from '../../enums/props.enum'; @@ -38,6 +46,8 @@ export class ExtensibleFormPropComponent implements OnChanges { @Input() prop: FormProp; + asterisk = ''; + options$: Observable[]> = of([]); validators: ValidatorFn[] = []; @@ -46,15 +56,55 @@ export class ExtensibleFormPropComponent implements OnChanges { disabled: boolean; - constructor(public readonly cdRef: ChangeDetectorRef, public readonly track: TrackByService) {} + private readonly form: FormGroup; + + typeaheadModel: any; - get asterisk(): string { - return this.validators.some(validator => validator === Validators.required) ? '*' : ''; + setTypeaheadValue(selectedOption: ABP.Option) { + 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 { - if (prop.options && prop.type !== ePropType.MultiSelect) return 'select'; + search = (text$: Observable) => + text$ + ? text$.pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap(text => this.prop.options(this.data, text)), + ) + : of([]); + + typeaheadFormatter = (option: ABP.Option) => 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 { [name + '_Text']: key, [name]: value } = this.form.controls; + return [key, value]; + } + + private setAsterisk() { + this.asterisk = this.validators.some(v => v === Validators.required) ? '*' : ''; + } + + getComponent(prop: FormProp): string { switch (prop.type) { case ePropType.Boolean: return 'checkbox'; @@ -62,14 +112,18 @@ export class ExtensibleFormPropComponent implements OnChanges { return 'date'; case ePropType.DateTime: return 'dateTime'; + case ePropType.Hidden: + return 'hidden'; + case ePropType.MultiSelect: + return 'multiselect'; case ePropType.Text: return 'textarea'; case ePropType.Time: return 'time'; - case ePropType.MultiSelect: - return 'multiselect'; + case ePropType.Typeahead: + return 'typeahead'; default: - return 'input'; + return prop.options ? 'select' : 'input'; } } @@ -92,14 +146,15 @@ export class ExtensibleFormPropComponent implements OnChanges { } ngOnChanges({ prop }: SimpleChanges) { - const options = prop.currentValue.options; - const readonly = prop.currentValue.readonly; - const disabled = prop.currentValue.disabled; - const validators = prop.currentValue.validators; + const currentProp = snq(() => prop.currentValue); + const { options, readonly, disabled, validators } = currentProp || {}; if (options) this.options$ = options(this.data); if (readonly) this.readonly = readonly(this.data); if (disabled) this.disabled = disabled(this.data); - if (validators) this.validators = validators(this.data); + if (validators) { + this.validators = validators(this.data); + this.setAsterisk(); + } } } From 0a1039332c923f1fe99b5a785b1a83418fe2e96b Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 00:48:11 +0300 Subject: [PATCH 06/12] feat: add styles for typeahead --- .../packages/theme-shared/src/lib/constants/styles.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts b/npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts index a54aecf581..cd9ac2a928 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts @@ -2,6 +2,11 @@ export default ` .is-invalid .form-control { border-color: #dc3545; 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, @@ -144,6 +149,9 @@ export default ` .ngx-datatable.material { box-shadow: none; } +ngb-typeahead-window { + width: 100%; +} @keyframes fadeInTop { from { From e2f773813b7db54ee8f8fd1f6cb8d98cf63315bc Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 02:21:12 +0300 Subject: [PATCH 07/12] feat: add typeahead utils to object extensions --- .../src/lib/utils/typeahead.util.ts | 43 +++++++++++ .../src/tests/typeahead.util.spec.ts | 72 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/typeahead.util.ts create mode 100644 npm/ng-packs/packages/theme-shared/extensions/src/tests/typeahead.util.spec.ts diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/typeahead.util.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/typeahead.util.ts new file mode 100644 index 0000000000..be89c4a295 --- /dev/null +++ b/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[]>> { + 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; +} diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/tests/typeahead.util.spec.ts b/npm/ng-packs/packages/theme-shared/extensions/src/tests/typeahead.util.spec.ts new file mode 100644 index 0000000000..479b1d4fcf --- /dev/null +++ b/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, + }; +} From e3ebd071754cf4f08a33a1a161197f2601b3c024 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 02:21:43 +0300 Subject: [PATCH 08/12] feat: handle typeahead in object extensions state utils --- .../extensions/src/lib/utils/state.util.ts | 12 ++- .../extensions/src/tests/state.util.spec.ts | 88 ++++++++++++++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts index 141d503f46..16d8b62785 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts +++ b/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, + ExtensionPropertyUiDto, +} from '@abp/ng.core'; import { Observable, pipe, zip } from 'rxjs'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { ePropType } from '../enums/props.enum'; @@ -9,6 +14,7 @@ import { PropCallback } from '../models/props'; import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util'; import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util'; import { createExtraPropertyValueResolver } from './props.util'; +import { createTypeaheadOptions, getTypeaheadType } from './typeahead.util'; import { getValidatorsFromProperty } from './validation.util'; function selectObjectExtensions( @@ -115,7 +121,8 @@ function createPropertiesToContributorsMapper( Object.keys(properties).forEach((name: string) => { const property = properties[name]; - const type = getTypeFromProperty(property); + const lookup = property.ui || ({} as ExtensionPropertyUiDto); + const type = getTypeaheadType(lookup, name) || getTypeFromProperty(property); const displayName = generateDisplayName(property.displayName, { name, resource }); if (property.ui.onTable.isVisible) { @@ -148,6 +155,7 @@ function createPropertiesToContributorsMapper( const validators = () => getValidatorsFromProperty(property); let options: PropCallback[]>>; if (type === ePropType.Enum) options = createEnumOptions(name, enums[property.type]); + else if (type === ePropType.Typeahead) options = createTypeaheadOptions(lookup); const formProp = new FormProp({ type, diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts b/npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts index 34acb30667..c0581aae45 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts @@ -50,24 +50,29 @@ describe('State Utils', () => { const propList = new EntityPropList(); 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.next.value.name).toBe('IsHero'); expect(propList.head.next.next.value.name).toBe('MyEnum'); + expect(propList.head.next.next.next.value.name).toBe('Foo_Text'); const createFormList = new FormPropList(); contributors.createForm.Role.forEach(callback => callback(createFormList)); - expect(createFormList.length).toBe(2); + expect(createFormList.length).toBe(4); expect(createFormList.head.value.name).toBe('Title'); expect(createFormList.head.next.value.name).toBe('MyEnum'); + expect(createFormList.head.next.next.value.name).toBe('Foo'); + expect(createFormList.head.next.next.next.value.name).toBe('Foo_Text'); const editFormList = new FormPropList(); contributors.editForm.Role.forEach(callback => callback(editFormList)); - expect(editFormList.length).toBe(2); + expect(editFormList.length).toBe(4); expect(editFormList.head.value.name).toBe('Title'); expect(editFormList.head.next.value.name).toBe('IsHero'); + expect(editFormList.head.next.next.value.name).toBe('Foo'); + expect(editFormList.head.next.next.next.value.name).toBe('Foo_Text'); }); }); }); @@ -275,6 +280,83 @@ function createMockEntities(): Record Date: Tue, 1 Dec 2020 02:29:46 +0300 Subject: [PATCH 09/12] fix: use correct dto for property ui lookup --- .../theme-shared/extensions/src/lib/utils/state.util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts index 16d8b62785..96f802af35 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts @@ -2,7 +2,7 @@ import { ABP, ApplicationLocalizationConfigurationDto, ConfigStateService, - ExtensionPropertyUiDto, + ExtensionPropertyUiLookupDto, } from '@abp/ng.core'; import { Observable, pipe, zip } from 'rxjs'; import { filter, map, switchMap, take } from 'rxjs/operators'; @@ -121,7 +121,7 @@ function createPropertiesToContributorsMapper( Object.keys(properties).forEach((name: string) => { const property = properties[name]; - const lookup = property.ui || ({} as ExtensionPropertyUiDto); + const lookup = property.ui.lookup || ({} as ExtensionPropertyUiLookupDto); const type = getTypeaheadType(lookup, name) || getTypeFromProperty(property); const displayName = generateDisplayName(property.displayName, { name, resource }); From 4f7167595ee8aa534a2d6a23dbcb27a8de0125ca Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 02:58:09 +0300 Subject: [PATCH 10/12] feat: add jsonNetCamelCase as case util --- .../extensions/src/lib/utils/case.util.ts | 37 +++++++++++++++++++ .../extensions/src/tests/case.util.spec.ts | 18 +++++++++ 2 files changed, 55 insertions(+) create mode 100644 npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/case.util.ts create mode 100644 npm/ng-packs/packages/theme-shared/extensions/src/tests/case.util.spec.ts diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/case.util.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/case.util.ts new file mode 100644 index 0000000000..5833b82de2 --- /dev/null +++ b/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(); +} diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/tests/case.util.spec.ts b/npm/ng-packs/packages/theme-shared/extensions/src/tests/case.util.spec.ts new file mode 100644 index 0000000000..db9f7b1de8 --- /dev/null +++ b/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); + }); + }); +}); From d090a5460f39ac07025d45d73631c998315dfbc0 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 02:58:57 +0300 Subject: [PATCH 11/12] fix: use jsonNetCamelCase on extra properties --- .../extensions/src/lib/utils/state.util.ts | 12 ++++++---- .../extensions/src/tests/state.util.spec.ts | 24 +++++++++---------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts index 96f802af35..938269e850 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts @@ -11,6 +11,7 @@ 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 { jsonNetCamelCase } from './case.util'; import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util'; import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util'; import { createExtraPropertyValueResolver } from './props.util'; @@ -121,6 +122,7 @@ function createPropertiesToContributorsMapper( Object.keys(properties).forEach((name: string) => { const property = properties[name]; + 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 }); @@ -130,12 +132,12 @@ function createPropertiesToContributorsMapper( const columnWidth = type === ePropType.Boolean ? 150 : 250; const valueResolver = type === ePropType.Enum - ? createEnumValueResolver(property.type, enums[property.type], name) - : createExtraPropertyValueResolver(name); + ? createEnumValueResolver(property.type, enums[property.type], propName) + : createExtraPropertyValueResolver(propName); const entityProp = new EntityProp({ type, - name, + name: propName, displayName, sortable, columnWidth, @@ -154,12 +156,12 @@ function createPropertiesToContributorsMapper( const defaultValue = property.defaultValue; const validators = () => getValidatorsFromProperty(property); let options: PropCallback[]>>; - 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({ type, - name, + name: propName, displayName, options, defaultValue, diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts b/npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts index c0581aae45..6f72a4a8b7 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts @@ -51,28 +51,28 @@ describe('State Utils', () => { contributors.prop.Role.forEach(callback => callback(propList)); expect(propList.length).toBe(4); - expect(propList.head.value.name).toBe('Title'); - expect(propList.head.next.value.name).toBe('IsHero'); - expect(propList.head.next.next.value.name).toBe('MyEnum'); - expect(propList.head.next.next.next.value.name).toBe('Foo_Text'); + expect(propList.head.value.name).toBe('title'); + expect(propList.head.next.value.name).toBe('isHero'); + expect(propList.head.next.next.value.name).toBe('myEnum'); + expect(propList.head.next.next.next.value.name).toBe('foo_Text'); const createFormList = new FormPropList(); contributors.createForm.Role.forEach(callback => callback(createFormList)); expect(createFormList.length).toBe(4); - expect(createFormList.head.value.name).toBe('Title'); - expect(createFormList.head.next.value.name).toBe('MyEnum'); - expect(createFormList.head.next.next.value.name).toBe('Foo'); - expect(createFormList.head.next.next.next.value.name).toBe('Foo_Text'); + expect(createFormList.head.value.name).toBe('title'); + expect(createFormList.head.next.value.name).toBe('myEnum'); + expect(createFormList.head.next.next.value.name).toBe('foo'); + expect(createFormList.head.next.next.next.value.name).toBe('foo_Text'); const editFormList = new FormPropList(); contributors.editForm.Role.forEach(callback => callback(editFormList)); expect(editFormList.length).toBe(4); - expect(editFormList.head.value.name).toBe('Title'); - expect(editFormList.head.next.value.name).toBe('IsHero'); - expect(editFormList.head.next.next.value.name).toBe('Foo'); - expect(editFormList.head.next.next.next.value.name).toBe('Foo_Text'); + expect(editFormList.head.value.name).toBe('title'); + expect(editFormList.head.next.value.name).toBe('isHero'); + expect(editFormList.head.next.next.value.name).toBe('foo'); + expect(editFormList.head.next.next.next.value.name).toBe('foo_Text'); }); }); }); From 019c27b5ab362773a8c3ebb962447f22086d92fd Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 1 Dec 2020 04:54:35 +0300 Subject: [PATCH 12/12] fix: get/set typeahead model and control values properly --- .../extensible-form-prop.component.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts index 60ae170957..658845c045 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts @@ -22,6 +22,7 @@ import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import snq from 'snq'; import { DateAdapter } from '../../adapters/date.adapter'; import { TimeAdapter } from '../../adapters/time.adapter'; +import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; import { ePropType } from '../../enums/props.enum'; import { FormProp } from '../../models/form-props'; import { PropData } from '../../models/props'; @@ -96,8 +97,12 @@ export class ExtensibleFormPropComponent implements OnChanges { private getTypeaheadControls() { const { name } = this.prop; - const { [name + '_Text']: key, [name]: value } = this.form.controls; - return [key, value]; + 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() { @@ -156,5 +161,9 @@ export class ExtensibleFormPropComponent implements OnChanges { this.validators = validators(this.data); this.setAsterisk(); } + + const [keyControl, valueControl] = this.getTypeaheadControls(); + if (keyControl && valueControl) + this.typeaheadModel = { key: keyControl.value, value: valueControl.value }; } }