Browse Source

UI: Added support js module resource in widget/action

pull/8562/head
Vladyslav_Prykhodko 3 years ago
parent
commit
1bde8d3dff
  1. 4
      ui-ngx/package.json
  2. 16
      ui-ngx/src/app/core/http/resource.service.ts
  3. 32
      ui-ngx/src/app/core/services/resources.service.ts
  4. 7
      ui-ngx/src/app/modules/common/modules-map.ts
  5. 32
      ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html
  6. 2
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  7. 7
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  8. 4
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  9. 30
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html
  10. 42
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts
  11. 31
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  12. 27
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss
  13. 1
      ui-ngx/src/app/shared/components/public-api.ts
  14. 46
      ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html
  15. 179
      ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts
  16. 3
      ui-ngx/src/app/shared/models/resource.models.ts
  17. 3
      ui-ngx/src/app/shared/shared.module.ts
  18. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  19. 16
      ui-ngx/yarn.lock

4
ui-ngx/package.json

@ -96,7 +96,7 @@
"schema-inspector": "^2.0.2",
"screenfull": "^6.0.2",
"split.js": "^1.6.5",
"systemjs": "6.11.0",
"systemjs": "6.14.1",
"tinycolor2": "^1.6.0",
"tinymce": "~5.10.7",
"tooltipster": "^4.2.8",
@ -137,7 +137,7 @@
"@types/raphael": "^2.3.2",
"@types/react": "17.0.37",
"@types/react-dom": "17.0.11",
"@types/systemjs": "6.1.1",
"@types/systemjs": "6.13.1",
"@types/tinycolor2": "^1.4.3",
"@types/tooltipster": "^0.0.31",
"@typescript-eslint/eslint-plugin": "5.57.0",

16
ui-ngx/src/app/core/http/resource.service.ts

@ -20,8 +20,9 @@ import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Resource, ResourceInfo } from '@shared/models/resource.models';
import { Resource, ResourceInfo, ResourceType } from '@shared/models/resource.models';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { isNotEmptyStr } from '@core/utils';
@Injectable({
providedIn: 'root'
@ -33,15 +34,22 @@ export class ResourceService {
}
public getResources(pageLink: PageLink, config?: RequestConfig): Observable<PageData<ResourceInfo>> {
return this.http.get<PageData<ResourceInfo>>(`/api/resource${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
public getResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable<PageData<ResourceInfo>> {
let url = `/api/resource${pageLink.toQuery()}`;
if (isNotEmptyStr(resourceType)) {
url += `&resourceType=${resourceType}`;
}
return this.http.get<PageData<ResourceInfo>>(url, defaultHttpOptionsFromConfig(config));
}
public getResource(resourceId: string, config?: RequestConfig): Observable<Resource> {
return this.http.get<Resource>(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
}
public getResourceInfo(resourceId: string, config?: RequestConfig): Observable<ResourceInfo> {
return this.http.get<Resource>(`/api/resource/info/${resourceId}`, defaultHttpOptionsFromConfig(config));
}
public downloadResource(resourceId: string): Observable<any> {
return this.http.get(`/api/resource/${resourceId}/download`, {
responseType: 'arraybuffer',

32
ui-ngx/src/app/core/services/resources.service.ts

@ -27,6 +27,9 @@ import { DOCUMENT } from '@angular/common';
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { IModulesMap } from '@modules/common/modules-map.models';
import { TbResourceId } from '@shared/models/id/tb-resource-id';
import { isObject } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
declare const System;
@ -69,16 +72,18 @@ export class ResourcesService {
return this.loadResourceByType(fileType, url);
}
public loadFactories(url: string, modulesMap: IModulesMap): Observable<ModulesWithFactories> {
public loadFactories(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<ModulesWithFactories> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModulesAndFactories[url]) {
return this.loadedModulesAndFactories[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<ModulesWithFactories>();
this.loadedModulesAndFactories[url] = subject;
import('@angular/compiler').then(
() => {
System.import(url).then(
System.import(url, undefined, meta).then(
(module) => {
const modules = this.extractNgModules(module);
if (modules.length) {
@ -123,16 +128,18 @@ export class ResourcesService {
return subject.asObservable();
}
public loadModules(url: string, modulesMap: IModulesMap): Observable<Type<any>[]> {
public loadModules(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<Type<any>[]> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModules[url]) {
return this.loadedModules[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<Type<any>[]>();
this.loadedModules[url] = subject;
import('@angular/compiler').then(
() => {
System.import(url).then(
System.import(url, undefined, meta).then(
(module) => {
try {
let modules;
@ -246,4 +253,21 @@ export class ResourcesService {
this.anchor.appendChild(el);
return subject.asObservable();
}
private getDownloadUrl(resourceId: string | TbResourceId): string {
if (isObject(resourceId)) {
return `/api/resource/js/${(resourceId as TbResourceId).id}/download`;
}
return resourceId as string;
}
private getMetaInfo(resourceId: string | TbResourceId): object {
if (isObject(resourceId)) {
return {
additionalHeaders: {
'X-Authorization': `Bearer ${AuthService.getJwtToken()}`
}
};
}
}
}

7
ui-ngx/src/app/modules/common/modules-map.ts

@ -614,6 +614,13 @@ class ModulesMap implements IModulesMap {
for (const moduleId of Object.keys(this.modulesMap)) {
System.set('app:' + moduleId, this.modulesMap[moduleId]);
}
System.constructor.prototype.shouldFetch = (url: string) => url.endsWith('/download');
System.constructor.prototype.fetch = (url, options: RequestInit & {meta?: any}) => {
if (options?.meta?.additionalHeaders) {
options.headers = { ...options.headers, ...options.meta.additionalHeaders };
}
return fetch(url, options);
};
this.initialized = true;
}
}

32
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html

@ -22,11 +22,14 @@
<div fxFlex fxLayout="row"
fxLayoutAlign="start center"
*ngFor="let resource of action.customResources; let i = index" style="padding-top: 10px;">
<mat-form-field fxFlex class="mat-block" subscriptSizing="dynamic">
<input required matInput [(ngModel)]="resource.url"
(ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<tb-resource-autocomplete fxFlex class="mat-block"
subscriptSizing="dynamic"
hideRequiredMarker required
[allowAutocomplete]="resource.isModule"
[(ngModel)]="resource.url"
(ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="notifyActionUpdated()">
{{ 'widget.resource-is-module' | translate }}
@ -40,16 +43,15 @@
<mat-icon>close</mat-icon>
</button>
</div>
<div style="margin-top: 18px;">
<button mat-button mat-raised-button color="primary"
[disabled]="isLoading$ | async"
type="button"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
<button mat-button mat-raised-button color="primary"
style="margin-top: 18px;"
[disabled]="isLoading$ | async"
type="button"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</div>
</mat-tab>

2
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component';
import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
import { QueueComponent } from '@home/pages/admin/queue/queue.component';
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
@ -44,6 +45,7 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut
OAuth2SettingsComponent,
HomeSettingsComponent,
ResourcesLibraryComponent,
ResourcesTableHeaderComponent,
QueueComponent,
RepositoryAdminSettingsComponent,
AutoCommitAdminSettingsComponent,

7
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -36,6 +36,7 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-
import { PageLink } from '@shared/models/page/page-link';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { map } from 'rxjs/operators';
import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
@Injectable()
export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource, PageLink, ResourceInfo>> {
@ -53,6 +54,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.entityComponent = ResourcesLibraryComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
this.config.headerComponent = ResourcesTableHeaderComponent;
this.config.entityTitle = (resource) => resource ?
resource.title : '';
@ -81,7 +83,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink);
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType);
this.config.loadEntity = id => this.resourceService.getResource(id.id);
this.config.saveEntity = resource => this.saveResource(resource);
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
@ -110,6 +112,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
resolve(): EntityTableConfig<Resource, PageLink, ResourceInfo> {
this.config.tableTitle = this.translate.instant('resource.resources-library');
this.config.componentsData = {
resourceType: ''
};
const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);

4
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -57,7 +57,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
ngOnInit() {
super.ngOnInit();
this.entityForm.get('resourceType').valueChanges.pipe(
startWith(ResourceType.LWM2M_MODEL),
startWith(ResourceType.JS_MODULE),
filter(() => this.isAdd),
takeUntil(this.destroy$)
).subscribe((type) => {
@ -91,7 +91,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
buildForm(entity: Resource): FormGroup {
return this.fb.group({
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required],
fileName: [entity ? entity.fileName : null, Validators.required],
data: [entity ? entity.data : null, Validators.required]
});

30
ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>resource.resource-type</mat-label>
<mat-select [ngModel]="entitiesTableConfig.componentsData.resourceType"
(ngModelChange)="resourceTypeChanged($event)"
placeholder="{{ 'resource.resource-type' | translate }}">
<mat-option value="">
{{ "resource.all-types" | translate }}
</mat-option>
<mat-option *ngFor="let resourceType of resourceTypes" [value]="resourceType">
{{ resourceTypesTranslationMap.get(resourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>

42
ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts

@ -0,0 +1,42 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import { Resource, ResourceInfo, ResourceType, ResourceTypeTranslationMap } from '@shared/models/resource.models';
import { PageLink } from '@shared/models/page/page-link';
@Component({
selector: 'tb-resources-table-header',
templateUrl: './resources-table-header.component.html',
styleUrls: []
})
export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent<Resource, PageLink, ResourceInfo> {
readonly resourceTypes: ResourceType[] = Object.values(ResourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
constructor(protected store: Store<AppState>) {
super(store);
}
resourceTypeChanged(resourceType: ResourceType) {
this.entitiesTableConfig.componentsData.resourceType = resourceType;
this.entitiesTableConfig.getTable().resetSortAndFilter(true);
}
}

31
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -122,12 +122,14 @@
<div fxFlex fxLayout="row" style="max-height: 40px;"
fxLayoutAlign="start center"
*ngFor="let resource of widget.resources; let i = index" >
<mat-form-field fxFlex class="mat-block resource-field" hideRequiredMarker="false"
style="margin: 10px 0 0 0; max-height: 40px;">
<input required matInput [(ngModel)]="resource.url"
(ngModelChange)="isDirty = true"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<tb-resource-autocomplete fxFlex class="mat-block resource-field"
hideRequiredMarker required
subscriptSizing="dynamic"
[allowAutocomplete]="resource.isModule"
[(ngModel)]="resource.url"
(ngModelChange)="isDirty = true"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="isDirty = true">
{{ 'widget.resource-is-module' | translate }}
@ -140,15 +142,14 @@
<mat-icon>close</mat-icon>
</button>
</div>
<div style="margin-top: 6px;">
<button mat-raised-button color="primary"
[disabled]="isLoading$ | async"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
<button mat-raised-button color="primary"
style="margin-top: 8px;"
[disabled]="isLoading$ | async"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</div>
</mat-tab>

27
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss

@ -31,18 +31,21 @@ tb-widget-editor {
overflow-y: auto;
}
mat-form-field.resource-field {
max-height: 40px;
margin: 10px 0 0;
.mat-mdc-text-field-wrapper {
padding-bottom: 0;
.mat-mdc-form-field-flex {
max-height: 40px;
.mat-mdc-form-field-infix {
border: 0;
padding-top: 7px;
padding-bottom: 7px;
min-height: 32px;
.resource-field {
mat-form-field {
.mat-mdc-text-field-wrapper {
padding-bottom: 0;
height: 40px;
.mat-mdc-form-field-flex {
max-height: 40px;
.mat-mdc-form-field-infix {
border: 0;
padding-top: 7px;
padding-bottom: 7px;
min-height: 32px;
}
}
}
}

1
ui-ngx/src/app/shared/components/public-api.ts

@ -22,4 +22,5 @@ export * from './js-func.component';
export * from './script-lang.component';
export * from './slack-conversation-autocomplete.component';
export * from './notification/template-autocomplete.component';
export * from './resource/resource-autocomplete.component';
export * from './toggle-header.component';

46
ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html

@ -0,0 +1,46 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field [formGroup]="resourceFormGroup" class="mat-block"
[appearance]="appearance"
[hideRequiredMarker]="hideRequiredMarker"
[subscriptSizing]="subscriptSizing">
<input matInput type="text"
#resourceInput
formControlName="resource"
(focusin)="onFocus()"
[placeholder]="placeholder"
[required]="required"
[matAutocomplete]="entityAutocomplete"
[matAutocompleteDisabled]="!allowAutocomplete">
<button *ngIf="resourceFormGroup.get('resource').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#entityAutocomplete="matAutocomplete"
[displayWith]="displayResourceFn">
<mat-option *ngFor="let resource of filteredResources$ | async" [value]="resource">
<span [innerHTML]="resource.title | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredResources$ | async)?.length" [value]="searchText">
{{ searchText }}
</mat-option>
</mat-autocomplete>
</mat-form-field>

179
ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts

@ -0,0 +1,179 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Observable, of } from 'rxjs';
import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
import { isDefinedAndNotNull, isEmptyStr, isEqual, isObject } from '@core/utils';
import { ResourceInfo, ResourceType } from '@shared/models/resource.models';
import { TbResourceId } from '@shared/models/id/tb-resource-id';
import { ResourceService } from '@core/http/resource.service';
import { PageLink } from '@shared/models/page/page-link';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-resource-autocomplete',
templateUrl: './resource-autocomplete.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ResourceAutocompleteComponent),
multi: true
}]
})
export class ResourceAutocompleteComponent implements ControlValueAccessor, OnInit {
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
@coerceBoolean()
required: boolean;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
placeholder: string;
@Input()
@coerceBoolean()
hideRequiredMarker = false;
@Input()
@coerceBoolean()
allowAutocomplete = false;
resourceFormGroup = this.fb.group({
resource: [null]
});
filteredResources$: Observable<Array<ResourceInfo>>;
searchText = '';
@ViewChild('resourceInput', {static: true}) resourceInput: ElementRef;
private modelValue: string | TbResourceId;
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder,
private resourceService: ResourceService) {
}
ngOnInit(): void {
if(this.required) {
this.resourceFormGroup.get('resource').setValidators(Validators.required);
this.resourceFormGroup.get('resource').updateValueAndValidity({emitEvent: false});
}
this.filteredResources$ = this.resourceFormGroup.get('resource').valueChanges
.pipe(
debounceTime(150),
tap(value => {
let modelValue;
if (isObject(value)) {
modelValue = value.id;
} else if (isEmptyStr(value)) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
if (value === null) {
this.clear();
}
}),
map(value => value ? (typeof value === 'string' ? value : value.title) : ''),
switchMap(name => this.fetchResources(name) ),
share()
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.resourceFormGroup.disable({emitEvent: false});
} else {
this.resourceFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string | TbResourceId) {
if (isDefinedAndNotNull(value)) {
this.searchText = '';
if (isObject(value) && typeof value !== 'string' && (value as TbResourceId).id) {
this.resourceService.getResourceInfo(value.id, {ignoreLoading: true, ignoreErrors: true}).subscribe(resource => {
this.modelValue = resource.id;
this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false});
});
} else {
this.modelValue = value;
this.resourceFormGroup.get('resource').patchValue(value, {emitEvent: false});
}
this.dirty = true;
}
}
displayResourceFn(resource?: ResourceInfo | string): string {
return isObject(resource) ? (resource as ResourceInfo).title : resource as string;
}
clear() {
this.resourceFormGroup.get('resource').patchValue('', {emitEvent: true});
setTimeout(() => {
this.resourceInput.nativeElement.blur();
this.resourceInput.nativeElement.focus();
}, 0);
}
onFocus() {
if (this.dirty) {
this.resourceFormGroup.get('resource').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
private updateView(value: string | TbResourceId ) {
if (!isEqual(this.modelValue, value)) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
private fetchResources(searchText?: string): Observable<Array<ResourceInfo>> {
this.searchText = searchText;
return this.resourceService.getResources(new PageLink(50, 0, searchText), ResourceType.JS_MODULE, {ignoreLoading: true}).pipe(
catchError(() => of(null)),
map(data => data.data)
);
}
}

3
ui-ngx/src/app/shared/models/resource.models.ts

@ -52,7 +52,7 @@ export const ResourceTypeTranslationMap = new Map<ResourceType, string>(
]
);
export interface ResourceInfo extends BaseData<TbResourceId> {
export interface ResourceInfo extends Omit<BaseData<TbResourceId>, 'name' | 'label'> {
tenantId?: TenantId;
resourceKey?: string;
title?: string;
@ -62,6 +62,7 @@ export interface ResourceInfo extends BaseData<TbResourceId> {
export interface Resource extends ResourceInfo {
data: string;
fileName: string;
name?: string;
}
export interface Resources extends ResourceInfo {

3
ui-ngx/src/app/shared/shared.module.ts

@ -189,6 +189,7 @@ import {
GtMdLgShowHideDirective
} from '@shared/layout/layout.directives';
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ResourceAutocompleteComponent } from '@shared/components/resource/resource-autocomplete.component';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
@ -360,6 +361,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ResourceAutocompleteComponent,
ToggleHeaderComponent
],
imports: [
@ -586,6 +588,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ResourceAutocompleteComponent,
ToggleHeaderComponent
]
})

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3252,6 +3252,7 @@
},
"resource": {
"add": "Add Resource",
"all-types": "All",
"copyId": "Copy resource Id",
"delete": "Delete resource",
"delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.",

16
ui-ngx/yarn.lock

@ -3286,10 +3286,10 @@
dependencies:
"@types/react" "*"
"@types/systemjs@6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-6.1.1.tgz#eae17f2a080e867d01a2dd614f524ab227cf5a41"
integrity sha512-d1M6eDKBGWx7RbYy295VEFoOF9YDJkPI959QYnmzcmeaV+SP4D0xV7dEh3sN5XF3GvO3PhGzm+17Z598nvHQuQ==
"@types/systemjs@6.13.1":
version "6.13.1"
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-6.13.1.tgz#fccf8049fdf328bca4cfbad3a9cc7bf088b45048"
integrity sha512-Jxo2/uif1WpkabfyvWpFmPWFPDdwKUmyL7xWzjtxNALEu2pgce+eISjbf0Vr+SsK/D9savO5kTRcf+COLK5eiQ==
"@types/tinycolor2@^1.4.3":
version "1.4.3"
@ -10101,10 +10101,10 @@ symbol-observable@4.0.0:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"
integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==
systemjs@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.11.0.tgz#8df8e74fc05822e6c40170aa409b9ca64833315f"
integrity sha512-7YPIY44j+BoY+E6cGBSw0oCU8SNTTIHKZgftcBdwWkDzs/M86Fdlr21FrzAyph7Zo8r3CFGscyFe4rrBtixrBg==
systemjs@6.14.1:
version "6.14.1"
resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.14.1.tgz#95a580b91b50d0d69ff178ed4816f0ddbcea23c1"
integrity sha512-8ftwWd+XnQtZ/aGbatrN4QFNGrKJzmbtixW+ODpci7pyoTajg4sonPP8aFLESAcuVxaC1FyDESt+SpfFCH9rZQ==
table-layout@^1.0.2:
version "1.0.2"

Loading…
Cancel
Save