UI: Added support js module resource in widget/action
This commit is contained in:
		
							parent
							
								
									87786ef72d
								
							
						
					
					
						commit
						1bde8d3dff
					
				@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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',
 | 
			
		||||
 | 
			
		||||
@ -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()}`
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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]
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -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';
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
@ -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)
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -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 {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -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.",
 | 
			
		||||
 | 
			
		||||
@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user