Commit 7e66cb938cb88dbd4f4369f540c101f6d1c584fc

Authored by Igor Kulikov
1 parent b0c5479c

UI: Model improvement, new entity table methods

@@ -28,7 +28,7 @@ import { @@ -28,7 +28,7 @@ import {
28 MissingTranslationHandler, 28 MissingTranslationHandler,
29 TranslateCompiler, 29 TranslateCompiler,
30 TranslateLoader, 30 TranslateLoader,
31 - TranslateModule 31 + TranslateModule, TranslateParser
32 } from '@ngx-translate/core'; 32 } from '@ngx-translate/core';
33 import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 33 import { TranslateHttpLoader } from '@ngx-translate/http-loader';
34 import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; 34 import { TbMissingTranslationHandler } from './translate/missing-translate-handler';
@@ -39,6 +39,7 @@ import { FlexLayoutModule } from '@angular/flex-layout'; @@ -39,6 +39,7 @@ import { FlexLayoutModule } from '@angular/flex-layout';
39 import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler'; 39 import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler';
40 import { WINDOW_PROVIDERS } from '@core/services/window.service'; 40 import { WINDOW_PROVIDERS } from '@core/services/window.service';
41 import { HotkeyModule } from 'angular2-hotkeys'; 41 import { HotkeyModule } from 'angular2-hotkeys';
  42 +import { TranslateDefaultParser } from '@core/translate/translate-default-parser';
42 43
43 export function HttpLoaderFactory(http: HttpClient) { 44 export function HttpLoaderFactory(http: HttpClient) {
44 return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); 45 return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json');
@@ -67,6 +68,10 @@ export function HttpLoaderFactory(http: HttpClient) { @@ -67,6 +68,10 @@ export function HttpLoaderFactory(http: HttpClient) {
67 compiler: { 68 compiler: {
68 provide: TranslateCompiler, 69 provide: TranslateCompiler,
69 useClass: TranslateDefaultCompiler 70 useClass: TranslateDefaultCompiler
  71 + },
  72 + parser: {
  73 + provide: TranslateParser,
  74 + useClass: TranslateDefaultParser
70 } 75 }
71 }), 76 }),
72 HotkeyModule.forRoot(), 77 HotkeyModule.forRoot(),
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Injectable } from '@angular/core';
  18 +import { TranslateParser } from '@ngx-translate/core';
  19 +import { isDefinedAndNotNull } from '@core/utils';
  20 +
  21 +@Injectable({ providedIn: 'root' })
  22 +export class TranslateDefaultParser extends TranslateParser {
  23 + templateMatcher: RegExp = /{{\s?([^{}\s]*)\s?}}/g;
  24 +
  25 + // tslint:disable-next-line:ban-types
  26 + public interpolate(expr: string | Function, params?: any): string {
  27 + let result: string;
  28 +
  29 + if (typeof expr === 'string') {
  30 + result = this.interpolateString(expr, params);
  31 + } else if (typeof expr === 'function') {
  32 + result = this.interpolateFunction(expr, params);
  33 + if (typeof result === 'string') {
  34 + result = this.interpolateString(result, params);
  35 + }
  36 + } else {
  37 + result = expr as string;
  38 + }
  39 +
  40 + return result;
  41 + }
  42 +
  43 + getValue(target: any, key: string): any {
  44 + const keys = typeof key === 'string' ? key.split('.') : [key];
  45 + key = '';
  46 + do {
  47 + key += keys.shift();
  48 + if (isDefinedAndNotNull(target) && isDefinedAndNotNull(target[key]) && (typeof target[key] === 'object' || !keys.length)) {
  49 + target = target[key];
  50 + key = '';
  51 + } else if (!keys.length) {
  52 + target = undefined;
  53 + } else {
  54 + key += '.';
  55 + }
  56 + } while (keys.length);
  57 +
  58 + return target;
  59 + }
  60 +
  61 + // tslint:disable-next-line:ban-types
  62 + private interpolateFunction(fn: Function, params?: any) {
  63 + return fn(params);
  64 + }
  65 +
  66 + private interpolateString(expr: string, params?: any) {
  67 + if (!params) {
  68 + return expr;
  69 + }
  70 +
  71 + return expr.replace(this.templateMatcher, (substring: string, b: string) => {
  72 + const r = this.getValue(params, b);
  73 + return isDefinedAndNotNull(r) ? r : substring;
  74 + });
  75 + }
  76 +}
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 16
17 import { 17 import {
18 AfterViewInit, 18 AfterViewInit,
19 - ChangeDetectionStrategy, 19 + ChangeDetectionStrategy, ChangeDetectorRef,
20 Component, 20 Component,
21 ComponentFactoryResolver, 21 ComponentFactoryResolver,
22 ElementRef, EventEmitter, 22 ElementRef, EventEmitter,
@@ -115,6 +115,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn @@ -115,6 +115,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
115 public dialog: MatDialog, 115 public dialog: MatDialog,
116 private dialogService: DialogService, 116 private dialogService: DialogService,
117 private domSanitizer: DomSanitizer, 117 private domSanitizer: DomSanitizer,
  118 + private cd: ChangeDetectorRef,
118 private componentFactoryResolver: ComponentFactoryResolver) { 119 private componentFactoryResolver: ComponentFactoryResolver) {
119 super(store); 120 super(store);
120 } 121 }
@@ -204,9 +205,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn @@ -204,9 +205,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
204 this.pageLink = new PageLink(10, 0, null, sortOrder); 205 this.pageLink = new PageLink(10, 0, null, sortOrder);
205 } 206 }
206 this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE; 207 this.pageLink.pageSize = this.displayPagination ? this.defaultPageSize : MAX_SAFE_PAGE_SIZE;
207 - this.dataSource = this.entitiesTableConfig.dataSource(() => {  
208 - this.dataLoaded();  
209 - }); 208 + this.dataSource = this.entitiesTableConfig.dataSource(this.dataLoaded.bind(this));
210 if (this.entitiesTableConfig.onLoadAction) { 209 if (this.entitiesTableConfig.onLoadAction) {
211 this.entitiesTableConfig.onLoadAction(this.route); 210 this.entitiesTableConfig.onLoadAction(this.route);
212 } 211 }
@@ -262,6 +261,11 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn @@ -262,6 +261,11 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
262 return this.entitiesTableConfig.addEnabled; 261 return this.entitiesTableConfig.addEnabled;
263 } 262 }
264 263
  264 + clearSelection() {
  265 + this.dataSource.selection.clear();
  266 + this.cd.detectChanges();
  267 + }
  268 +
265 updateData(closeDetails: boolean = true) { 269 updateData(closeDetails: boolean = true) {
266 if (closeDetails) { 270 if (closeDetails) {
267 this.isDetailsOpen = false; 271 this.isDetailsOpen = false;
@@ -294,11 +298,15 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn @@ -294,11 +298,15 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
294 this.dataSource.loadEntities(this.pageLink); 298 this.dataSource.loadEntities(this.pageLink);
295 } 299 }
296 300
297 - private dataLoaded() {  
298 - this.headerCellStyleCache.length = 0;  
299 - this.cellContentCache.length = 0;  
300 - this.cellTooltipCache.length = 0;  
301 - this.cellStyleCache.length = 0; 301 + private dataLoaded(col?: number, row?: number) {
  302 + if (isFinite(col) && isFinite(row)) {
  303 + this.clearCellCache(col, row);
  304 + } else {
  305 + this.headerCellStyleCache.length = 0;
  306 + this.cellContentCache.length = 0;
  307 + this.cellTooltipCache.length = 0;
  308 + this.cellStyleCache.length = 0;
  309 + }
302 } 310 }
303 311
304 onRowClick($event: Event, entity) { 312 onRowClick($event: Event, entity) {
@@ -485,6 +493,13 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn @@ -485,6 +493,13 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn
485 return res; 493 return res;
486 } 494 }
487 495
  496 + clearCellCache(col: number, row: number) {
  497 + const index = row * this.entitiesTableConfig.columns.length + col;
  498 + this.cellContentCache[index] = undefined;
  499 + this.cellTooltipCache[index] = undefined;
  500 + this.cellStyleCache[index] = undefined;
  501 + }
  502 +
488 cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) { 503 cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) {
489 if (column instanceof EntityTableColumn) { 504 if (column instanceof EntityTableColumn) {
490 const col = this.entitiesTableConfig.columns.indexOf(column); 505 const col = this.entitiesTableConfig.columns.indexOf(column);
@@ -37,7 +37,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = @@ -37,7 +37,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
37 37
38 constructor(private fetchFunction: EntitiesFetchFunction<T, P>, 38 constructor(private fetchFunction: EntitiesFetchFunction<T, P>,
39 protected selectionEnabledFunction: EntityBooleanFunction<T>, 39 protected selectionEnabledFunction: EntityBooleanFunction<T>,
40 - protected dataLoadedFunction: () => void) {} 40 + protected dataLoadedFunction: (col?: number, row?: number) => void) {}
41 41
42 connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { 42 connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> {
43 return this.entitiesSubject.asObservable(); 43 return this.entitiesSubject.asObservable();
@@ -50,7 +50,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = @@ -50,7 +50,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
50 50
51 reset() { 51 reset() {
52 const pageData = emptyPageData<T>(); 52 const pageData = emptyPageData<T>();
53 - this.entitiesSubject.next(pageData.data); 53 + this.onEntities(pageData.data);
54 this.pageDataSubject.next(pageData); 54 this.pageDataSubject.next(pageData);
55 this.dataLoadedFunction(); 55 this.dataLoadedFunction();
56 } 56 }
@@ -64,7 +64,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = @@ -64,7 +64,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
64 catchError(() => of(emptyPageData<T>())), 64 catchError(() => of(emptyPageData<T>())),
65 ).subscribe( 65 ).subscribe(
66 (pageData) => { 66 (pageData) => {
67 - this.entitiesSubject.next(pageData.data); 67 + this.onEntities(pageData.data);
68 this.pageDataSubject.next(pageData); 68 this.pageDataSubject.next(pageData);
69 result.next(pageData); 69 result.next(pageData);
70 this.dataLoadedFunction(); 70 this.dataLoadedFunction();
@@ -73,6 +73,10 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = @@ -73,6 +73,10 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink =
73 return result; 73 return result;
74 } 74 }
75 75
  76 + protected onEntities(entities: T[]) {
  77 + this.entitiesSubject.next(entities);
  78 + }
  79 +
76 isAllSelected(): Observable<boolean> { 80 isAllSelected(): Observable<boolean> {
77 const numSelected = this.selection.selected.length; 81 const numSelected = this.selection.selected.length;
78 return this.entitiesSubject.pipe( 82 return this.entitiesSubject.pipe(
@@ -157,7 +157,8 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P @@ -157,7 +157,8 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P
157 addActionDescriptors: Array<HeaderActionDescriptor> = []; 157 addActionDescriptors: Array<HeaderActionDescriptor> = [];
158 headerComponent: Type<EntityTableHeaderComponent<T, P, L>>; 158 headerComponent: Type<EntityTableHeaderComponent<T, P, L>>;
159 addEntity: CreateEntityOperation<T> = null; 159 addEntity: CreateEntityOperation<T> = null;
160 - dataSource: (dataLoadedFunction: () => void) => EntitiesDataSource<L> = (dataLoadedFunction: () => void) => { 160 + dataSource: (dataLoadedFunction: (col?: number, row?: number) => void)
  161 + => EntitiesDataSource<L> = (dataLoadedFunction: (col?: number, row?: number) => void) => {
161 return new EntitiesDataSource(this.entitiesFetchFunction, this.entitySelectionEnabled, dataLoadedFunction); 162 return new EntitiesDataSource(this.entitiesFetchFunction, this.entitySelectionEnabled, dataLoadedFunction);
162 }; 163 };
163 detailsReadonly: EntityBooleanFunction<T> = () => false; 164 detailsReadonly: EntityBooleanFunction<T> = () => false;
@@ -279,6 +279,10 @@ export class TelemetrySubscriber { @@ -279,6 +279,10 @@ export class TelemetrySubscriber {
279 279
280 public unsubscribe() { 280 public unsubscribe() {
281 this.telemetryService.unsubscribe(this); 281 this.telemetryService.unsubscribe(this);
  282 + this.complete();
  283 + }
  284 +
  285 + public complete() {
282 this.dataSubject.complete(); 286 this.dataSubject.complete();
283 this.reconnectSubject.complete(); 287 this.reconnectSubject.complete();
284 } 288 }