Commit 7e66cb938cb88dbd4f4369f540c101f6d1c584fc
1 parent
b0c5479c
UI: Model improvement, new entity table methods
Showing
6 changed files
with
119 additions
and
14 deletions
... | ... | @@ -28,7 +28,7 @@ import { |
28 | 28 | MissingTranslationHandler, |
29 | 29 | TranslateCompiler, |
30 | 30 | TranslateLoader, |
31 | - TranslateModule | |
31 | + TranslateModule, TranslateParser | |
32 | 32 | } from '@ngx-translate/core'; |
33 | 33 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; |
34 | 34 | import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; |
... | ... | @@ -39,6 +39,7 @@ import { FlexLayoutModule } from '@angular/flex-layout'; |
39 | 39 | import { TranslateDefaultCompiler } from '@core/translate/translate-default-compiler'; |
40 | 40 | import { WINDOW_PROVIDERS } from '@core/services/window.service'; |
41 | 41 | import { HotkeyModule } from 'angular2-hotkeys'; |
42 | +import { TranslateDefaultParser } from '@core/translate/translate-default-parser'; | |
42 | 43 | |
43 | 44 | export function HttpLoaderFactory(http: HttpClient) { |
44 | 45 | return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); |
... | ... | @@ -67,6 +68,10 @@ export function HttpLoaderFactory(http: HttpClient) { |
67 | 68 | compiler: { |
68 | 69 | provide: TranslateCompiler, |
69 | 70 | useClass: TranslateDefaultCompiler |
71 | + }, | |
72 | + parser: { | |
73 | + provide: TranslateParser, | |
74 | + useClass: TranslateDefaultParser | |
70 | 75 | } |
71 | 76 | }), |
72 | 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 | 16 | |
17 | 17 | import { |
18 | 18 | AfterViewInit, |
19 | - ChangeDetectionStrategy, | |
19 | + ChangeDetectionStrategy, ChangeDetectorRef, | |
20 | 20 | Component, |
21 | 21 | ComponentFactoryResolver, |
22 | 22 | ElementRef, EventEmitter, |
... | ... | @@ -115,6 +115,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
115 | 115 | public dialog: MatDialog, |
116 | 116 | private dialogService: DialogService, |
117 | 117 | private domSanitizer: DomSanitizer, |
118 | + private cd: ChangeDetectorRef, | |
118 | 119 | private componentFactoryResolver: ComponentFactoryResolver) { |
119 | 120 | super(store); |
120 | 121 | } |
... | ... | @@ -204,9 +205,7 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
204 | 205 | this.pageLink = new PageLink(10, 0, null, sortOrder); |
205 | 206 | } |
206 | 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 | 209 | if (this.entitiesTableConfig.onLoadAction) { |
211 | 210 | this.entitiesTableConfig.onLoadAction(this.route); |
212 | 211 | } |
... | ... | @@ -262,6 +261,11 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
262 | 261 | return this.entitiesTableConfig.addEnabled; |
263 | 262 | } |
264 | 263 | |
264 | + clearSelection() { | |
265 | + this.dataSource.selection.clear(); | |
266 | + this.cd.detectChanges(); | |
267 | + } | |
268 | + | |
265 | 269 | updateData(closeDetails: boolean = true) { |
266 | 270 | if (closeDetails) { |
267 | 271 | this.isDetailsOpen = false; |
... | ... | @@ -294,11 +298,15 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
294 | 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 | 312 | onRowClick($event: Event, entity) { |
... | ... | @@ -485,6 +493,13 @@ export class EntitiesTableComponent extends PageComponent implements AfterViewIn |
485 | 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 | 503 | cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) { |
489 | 504 | if (column instanceof EntityTableColumn) { |
490 | 505 | const col = this.entitiesTableConfig.columns.indexOf(column); | ... | ... |
... | ... | @@ -37,7 +37,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = |
37 | 37 | |
38 | 38 | constructor(private fetchFunction: EntitiesFetchFunction<T, P>, |
39 | 39 | protected selectionEnabledFunction: EntityBooleanFunction<T>, |
40 | - protected dataLoadedFunction: () => void) {} | |
40 | + protected dataLoadedFunction: (col?: number, row?: number) => void) {} | |
41 | 41 | |
42 | 42 | connect(collectionViewer: CollectionViewer): Observable<T[] | ReadonlyArray<T>> { |
43 | 43 | return this.entitiesSubject.asObservable(); |
... | ... | @@ -50,7 +50,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = |
50 | 50 | |
51 | 51 | reset() { |
52 | 52 | const pageData = emptyPageData<T>(); |
53 | - this.entitiesSubject.next(pageData.data); | |
53 | + this.onEntities(pageData.data); | |
54 | 54 | this.pageDataSubject.next(pageData); |
55 | 55 | this.dataLoadedFunction(); |
56 | 56 | } |
... | ... | @@ -64,7 +64,7 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = |
64 | 64 | catchError(() => of(emptyPageData<T>())), |
65 | 65 | ).subscribe( |
66 | 66 | (pageData) => { |
67 | - this.entitiesSubject.next(pageData.data); | |
67 | + this.onEntities(pageData.data); | |
68 | 68 | this.pageDataSubject.next(pageData); |
69 | 69 | result.next(pageData); |
70 | 70 | this.dataLoadedFunction(); |
... | ... | @@ -73,6 +73,10 @@ export class EntitiesDataSource<T extends BaseData<HasId>, P extends PageLink = |
73 | 73 | return result; |
74 | 74 | } |
75 | 75 | |
76 | + protected onEntities(entities: T[]) { | |
77 | + this.entitiesSubject.next(entities); | |
78 | + } | |
79 | + | |
76 | 80 | isAllSelected(): Observable<boolean> { |
77 | 81 | const numSelected = this.selection.selected.length; |
78 | 82 | return this.entitiesSubject.pipe( | ... | ... |
... | ... | @@ -157,7 +157,8 @@ export class EntityTableConfig<T extends BaseData<HasId>, P extends PageLink = P |
157 | 157 | addActionDescriptors: Array<HeaderActionDescriptor> = []; |
158 | 158 | headerComponent: Type<EntityTableHeaderComponent<T, P, L>>; |
159 | 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 | 162 | return new EntitiesDataSource(this.entitiesFetchFunction, this.entitySelectionEnabled, dataLoadedFunction); |
162 | 163 | }; |
163 | 164 | detailsReadonly: EntityBooleanFunction<T> = () => false; | ... | ... |
... | ... | @@ -279,6 +279,10 @@ export class TelemetrySubscriber { |
279 | 279 | |
280 | 280 | public unsubscribe() { |
281 | 281 | this.telemetryService.unsubscribe(this); |
282 | + this.complete(); | |
283 | + } | |
284 | + | |
285 | + public complete() { | |
282 | 286 | this.dataSubject.complete(); |
283 | 287 | this.reconnectSubject.complete(); |
284 | 288 | } | ... | ... |