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"
|
||||
<tb-resource-autocomplete fxFlex class="mat-block"
|
||||
subscriptSizing="dynamic"
|
||||
hideRequiredMarker required
|
||||
[allowAutocomplete]="resource.isModule"
|
||||
[(ngModel)]="resource.url"
|
||||
(ngModelChange)="notifyActionUpdated()"
|
||||
placeholder="{{ 'widget.resource-url' | translate }}"/>
|
||||
</mat-form-field>
|
||||
placeholder="{{ 'widget.resource-url' | translate }}">
|
||||
</tb-resource-autocomplete>
|
||||
<mat-checkbox [(ngModel)]="resource.isModule"
|
||||
(ngModelChange)="notifyActionUpdated()">
|
||||
{{ 'widget.resource-is-module' | translate }}
|
||||
@ -40,8 +43,8 @@
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-top: 18px;">
|
||||
<button mat-button mat-raised-button color="primary"
|
||||
style="margin-top: 18px;"
|
||||
[disabled]="isLoading$ | async"
|
||||
type="button"
|
||||
(click)="addResource()"
|
||||
@ -51,7 +54,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;">
|
||||
<div class="tb-custom-action-editor-container" tb-fullscreen [fullscreen]="cssFullscreen">
|
||||
|
||||
@ -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"
|
||||
<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 }}"/>
|
||||
</mat-form-field>
|
||||
placeholder="{{ 'widget.resource-url' | translate }}">
|
||||
</tb-resource-autocomplete>
|
||||
<mat-checkbox [(ngModel)]="resource.isModule"
|
||||
(ngModelChange)="isDirty = true">
|
||||
{{ 'widget.resource-is-module' | translate }}
|
||||
@ -140,8 +142,8 @@
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-top: 6px;">
|
||||
<button mat-raised-button color="primary"
|
||||
style="margin-top: 8px;"
|
||||
[disabled]="isLoading$ | async"
|
||||
(click)="addResource()"
|
||||
matTooltip="{{'widget.add-resource' | translate}}"
|
||||
@ -150,7 +152,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab label="{{ 'widget.html' | translate }}">
|
||||
<div class="tb-resize-container" tb-fullscreen [fullscreen]="htmlFullscreen">
|
||||
|
||||
@ -31,13 +31,15 @@ tb-widget-editor {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
mat-form-field.resource-field {
|
||||
max-height: 40px;
|
||||
margin: 10px 0 0;
|
||||
.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;
|
||||
@ -47,6 +49,7 @@ tb-widget-editor {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ace_editor {
|
||||
font-size: 14px !important;
|
||||
|
||||
@ -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