Commit 2eb93dac7e0c83b98fa01e63f4409be00ed3295c
1 parent
b60b3144
Widget Editor: Save and save as actions.
Showing
24 changed files
with
821 additions
and
53 deletions
... | ... | @@ -16,7 +16,7 @@ |
16 | 16 | |
17 | 17 | import { Injectable } from '@angular/core'; |
18 | 18 | import { defaultHttpOptions } from './http-utils'; |
19 | -import { Observable } from 'rxjs/index'; | |
19 | +import { Observable, Subject, of, ReplaySubject } from 'rxjs/index'; | |
20 | 20 | import { HttpClient } from '@angular/common/http'; |
21 | 21 | import { PageLink } from '@shared/models/page/page-link'; |
22 | 22 | import { PageData } from '@shared/models/page/page-data'; |
... | ... | @@ -25,20 +25,57 @@ import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared |
25 | 25 | import { UtilsService } from '@core/services/utils.service'; |
26 | 26 | import { TranslateService } from '@ngx-translate/core'; |
27 | 27 | import { ResourcesService } from '../services/resources.service'; |
28 | -import { toWidgetInfo, WidgetInfo } from '@app/modules/home/models/widget-component.models'; | |
29 | -import { map } from 'rxjs/operators'; | |
28 | +import { toWidgetInfo, WidgetInfo, toWidgetType } from '@app/modules/home/models/widget-component.models'; | |
29 | +import { map, tap, mergeMap, filter } from 'rxjs/operators'; | |
30 | +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; | |
31 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | |
32 | +import { ActivationEnd, Router } from '@angular/router'; | |
30 | 33 | |
31 | 34 | @Injectable({ |
32 | 35 | providedIn: 'root' |
33 | 36 | }) |
34 | 37 | export class WidgetService { |
35 | 38 | |
39 | + private widgetTypeUpdatedSubject = new Subject<WidgetType>(); | |
40 | + private widgetsBundleDeletedSubject = new Subject<WidgetsBundle>(); | |
41 | + | |
42 | + private allWidgetsBundles: Array<WidgetsBundle>; | |
43 | + private systemWidgetsBundles: Array<WidgetsBundle>; | |
44 | + private tenantWidgetsBundles: Array<WidgetsBundle>; | |
45 | + | |
36 | 46 | constructor( |
37 | 47 | private http: HttpClient, |
38 | 48 | private utils: UtilsService, |
39 | 49 | private resources: ResourcesService, |
40 | - private translate: TranslateService | |
50 | + private translate: TranslateService, | |
51 | + private router: Router | |
41 | 52 | ) { |
53 | + this.router.events.pipe(filter(event => event instanceof ActivationEnd)).subscribe( | |
54 | + () => { | |
55 | + this.invalidateWidgetsBundleCache(); | |
56 | + } | |
57 | + ); | |
58 | + } | |
59 | + | |
60 | + public getAllWidgetsBundles(ignoreErrors: boolean = false, | |
61 | + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> { | |
62 | + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( | |
63 | + map(() => this.allWidgetsBundles) | |
64 | + ); | |
65 | + } | |
66 | + | |
67 | + public getSystemWidgetsBundles(ignoreErrors: boolean = false, | |
68 | + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> { | |
69 | + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( | |
70 | + map(() => this.systemWidgetsBundles) | |
71 | + ); | |
72 | + } | |
73 | + | |
74 | + public getTenantWidgetsBundles(ignoreErrors: boolean = false, | |
75 | + ignoreLoading: boolean = false): Observable<Array<WidgetsBundle>> { | |
76 | + return this.loadWidgetsBundleCache(ignoreErrors, ignoreLoading).pipe( | |
77 | + map(() => this.tenantWidgetsBundles) | |
78 | + ); | |
42 | 79 | } |
43 | 80 | |
44 | 81 | public getWidgetBundles(pageLink: PageLink, ignoreErrors: boolean = false, |
... | ... | @@ -54,11 +91,26 @@ export class WidgetService { |
54 | 91 | |
55 | 92 | public saveWidgetsBundle(widgetsBundle: WidgetsBundle, |
56 | 93 | ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetsBundle> { |
57 | - return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
94 | + return this.http.post<WidgetsBundle>('/api/widgetsBundle', widgetsBundle, | |
95 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | |
96 | + tap(() => { | |
97 | + this.invalidateWidgetsBundleCache(); | |
98 | + }) | |
99 | + ); | |
58 | 100 | } |
59 | 101 | |
60 | 102 | public deleteWidgetsBundle(widgetsBundleId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { |
61 | - return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); | |
103 | + return this.getWidgetsBundle(widgetsBundleId, ignoreErrors, ignoreLoading).pipe( | |
104 | + mergeMap((widgetsBundle) => { | |
105 | + return this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, | |
106 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | |
107 | + tap(() => { | |
108 | + this.invalidateWidgetsBundleCache(); | |
109 | + this.widgetsBundleDeletedSubject.next(widgetsBundle); | |
110 | + }) | |
111 | + ); | |
112 | + } | |
113 | + )); | |
62 | 114 | } |
63 | 115 | |
64 | 116 | public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean, |
... | ... | @@ -73,6 +125,41 @@ export class WidgetService { |
73 | 125 | defaultHttpOptions(ignoreLoading, ignoreErrors)); |
74 | 126 | } |
75 | 127 | |
128 | + public saveWidgetType(widgetInfo: WidgetInfo, | |
129 | + id: WidgetTypeId, | |
130 | + bundleAlias: string, | |
131 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { | |
132 | + const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias); | |
133 | + return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance, | |
134 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | |
135 | + tap((savedWidgetType) => { | |
136 | + this.widgetTypeUpdatedSubject.next(savedWidgetType); | |
137 | + })); | |
138 | + } | |
139 | + | |
140 | + public saveImportedWidgetType(widgetTypeInstance: WidgetType, | |
141 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { | |
142 | + return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance, | |
143 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | |
144 | + tap((savedWidgetType) => { | |
145 | + this.widgetTypeUpdatedSubject.next(savedWidgetType); | |
146 | + })); | |
147 | + } | |
148 | + | |
149 | + public deleteWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, | |
150 | + ignoreErrors: boolean = false, ignoreLoading: boolean = false) { | |
151 | + return this.getWidgetType(bundleAlias, widgetTypeAlias, isSystem, ignoreErrors, ignoreLoading).pipe( | |
152 | + mergeMap((widgetTypeInstance) => { | |
153 | + return this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`, | |
154 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).pipe( | |
155 | + tap(() => { | |
156 | + this.widgetTypeUpdatedSubject.next(widgetTypeInstance); | |
157 | + }) | |
158 | + ); | |
159 | + } | |
160 | + )); | |
161 | + } | |
162 | + | |
76 | 163 | public getWidgetTypeById(widgetTypeId: string, |
77 | 164 | ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { |
78 | 165 | return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`, |
... | ... | @@ -90,5 +177,55 @@ export class WidgetService { |
90 | 177 | return widgetInfo; |
91 | 178 | }) |
92 | 179 | ); |
180 | + } | |
181 | + | |
182 | + public onWidgetTypeUpdated(): Observable<WidgetType> { | |
183 | + return this.widgetTypeUpdatedSubject.asObservable(); | |
184 | + } | |
185 | + | |
186 | + public onWidgetBundleDeleted(): Observable<WidgetsBundle> { | |
187 | + return this.widgetsBundleDeletedSubject.asObservable(); | |
188 | + } | |
189 | + | |
190 | + private loadWidgetsBundleCache(ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<any> { | |
191 | + if (!this.allWidgetsBundles) { | |
192 | + const loadWidgetsBundleCacheSubject = new ReplaySubject(); | |
193 | + this.http.get<Array<WidgetsBundle>>('/api/widgetsBundles', | |
194 | + defaultHttpOptions(ignoreLoading, ignoreErrors)).subscribe( | |
195 | + (allWidgetsBundles) => { | |
196 | + this.allWidgetsBundles = allWidgetsBundles; | |
197 | + this.systemWidgetsBundles = new Array<WidgetsBundle>(); | |
198 | + this.tenantWidgetsBundles = new Array<WidgetsBundle>(); | |
199 | + this.allWidgetsBundles = this.allWidgetsBundles.sort((wb1, wb2) => { | |
200 | + let res = wb1.title.localeCompare(wb2.title); | |
201 | + if (res === 0) { | |
202 | + res = wb2.createdTime - wb1.createdTime; | |
203 | + } | |
204 | + return res; | |
205 | + }); | |
206 | + this.allWidgetsBundles.forEach((widgetsBundle) => { | |
207 | + if (widgetsBundle.tenantId.id === NULL_UUID) { | |
208 | + this.systemWidgetsBundles.push(widgetsBundle); | |
209 | + } else { | |
210 | + this.tenantWidgetsBundles.push(widgetsBundle); | |
211 | + } | |
212 | + }); | |
213 | + loadWidgetsBundleCacheSubject.next(); | |
214 | + loadWidgetsBundleCacheSubject.complete(); | |
215 | + }, | |
216 | + () => { | |
217 | + loadWidgetsBundleCacheSubject.error(null); | |
218 | + }); | |
219 | + return loadWidgetsBundleCacheSubject.asObservable(); | |
220 | + } else { | |
221 | + return of(null); | |
93 | 222 | } |
223 | + } | |
224 | + | |
225 | + private invalidateWidgetsBundleCache() { | |
226 | + this.allWidgetsBundles = undefined; | |
227 | + this.systemWidgetsBundles = undefined; | |
228 | + this.tenantWidgetsBundles = undefined; | |
229 | + } | |
230 | + | |
94 | 231 | } | ... | ... |
... | ... | @@ -84,7 +84,7 @@ export class UtilsService { |
84 | 84 | } |
85 | 85 | |
86 | 86 | public processWidgetException(exception: any): ExceptionData { |
87 | - const data = this.parseException(exception, -5); | |
87 | + const data = this.parseException(exception, -6); | |
88 | 88 | if (this.widgetEditMode) { |
89 | 89 | const message: WindowMessage = { |
90 | 90 | type: 'widgetException', | ... | ... |
... | ... | @@ -308,7 +308,13 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo |
308 | 308 | $event.stopPropagation(); |
309 | 309 | } |
310 | 310 | if (this.isRemoveActionEnabled && this.callbacks && this.callbacks.onRemoveWidget) { |
311 | - this.callbacks.onRemoveWidget($event, widget.widget); | |
311 | + this.callbacks.onRemoveWidget($event, widget.widget).subscribe( | |
312 | + (result) => { | |
313 | + if (result) { | |
314 | + this.dashboardWidgets.removeWidget(widget.widget); | |
315 | + } | |
316 | + } | |
317 | + ); | |
312 | 318 | } |
313 | 319 | } |
314 | 320 | ... | ... |
... | ... | @@ -18,16 +18,19 @@ import { Inject, Injectable } from '@angular/core'; |
18 | 18 | import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; |
19 | 19 | import { WidgetService } from '@core/http/widget.service'; |
20 | 20 | import { forkJoin, Observable, of, ReplaySubject, Subject, throwError } from 'rxjs'; |
21 | -import { WidgetInfo, MissingWidgetType, toWidgetInfo, WidgetTypeInstance, ErrorWidgetType } from '@home/models/widget-component.models'; | |
21 | +import { | |
22 | + ErrorWidgetType, | |
23 | + MissingWidgetType, | |
24 | + toWidgetInfo, | |
25 | + toWidgetType, | |
26 | + WidgetInfo, | |
27 | + WidgetTypeInstance | |
28 | +} from '@home/models/widget-component.models'; | |
22 | 29 | import cssjs from '@core/css/css'; |
23 | 30 | import { UtilsService } from '@core/services/utils.service'; |
24 | 31 | import { ResourcesService } from '@core/services/resources.service'; |
25 | -import { | |
26 | - widgetActionSources, | |
27 | - WidgetControllerDescriptor, | |
28 | - WidgetType | |
29 | -} from '@shared/models/widget.models'; | |
30 | -import { catchError, switchMap, map, mergeMap } from 'rxjs/operators'; | |
32 | +import { widgetActionSources, WidgetControllerDescriptor, WidgetType } from '@shared/models/widget.models'; | |
33 | +import { catchError, map, mergeMap, switchMap } from 'rxjs/operators'; | |
31 | 34 | import { isFunction, isUndefined } from '@core/utils'; |
32 | 35 | import { TranslateService } from '@ngx-translate/core'; |
33 | 36 | import { DynamicWidgetComponent } from '@home/components/widget/dynamic-widget.component'; |
... | ... | @@ -37,6 +40,9 @@ import { WINDOW } from '@core/services/window.service'; |
37 | 40 | |
38 | 41 | import * as tinycolor from 'tinycolor2'; |
39 | 42 | import { TbFlot } from './lib/flot-widget'; |
43 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | |
44 | +import { WidgetTypeId } from '@app/shared/models/id/widget-type-id'; | |
45 | +import { TenantId } from '@app/shared/models/id/tenant-id'; | |
40 | 46 | |
41 | 47 | // declare var jQuery: any; |
42 | 48 | |
... | ... | @@ -53,6 +59,7 @@ export class WidgetComponentService { |
53 | 59 | |
54 | 60 | private missingWidgetType: WidgetInfo; |
55 | 61 | private errorWidgetType: WidgetInfo; |
62 | + private editingWidgetType: WidgetType; | |
56 | 63 | |
57 | 64 | constructor(@Inject(WINDOW) private window: Window, |
58 | 65 | private dynamicComponentFactoryService: DynamicComponentFactoryService, |
... | ... | @@ -68,6 +75,15 @@ export class WidgetComponentService { |
68 | 75 | this.window.TbFlot = TbFlot; |
69 | 76 | |
70 | 77 | this.cssParser.testMode = false; |
78 | + | |
79 | + this.widgetService.onWidgetTypeUpdated().subscribe((widgetType) => { | |
80 | + this.deleteWidgetInfoFromCache(widgetType.bundleAlias, widgetType.alias, widgetType.tenantId.id === NULL_UUID); | |
81 | + }); | |
82 | + | |
83 | + this.widgetService.onWidgetBundleDeleted().subscribe((widgetsBundle) => { | |
84 | + this.deleteWidgetsBundleFromCache(widgetsBundle.alias, widgetsBundle.tenantId.id === NULL_UUID); | |
85 | + }); | |
86 | + | |
71 | 87 | this.init(); |
72 | 88 | } |
73 | 89 | |
... | ... | @@ -77,6 +93,24 @@ export class WidgetComponentService { |
77 | 93 | } else { |
78 | 94 | this.missingWidgetType = {...MissingWidgetType}; |
79 | 95 | this.errorWidgetType = {...ErrorWidgetType}; |
96 | + if (this.utils.widgetEditMode) { | |
97 | + this.editingWidgetType = toWidgetType( | |
98 | + { | |
99 | + widgetName: this.utils.editWidgetInfo.widgetName, | |
100 | + alias: 'customWidget', | |
101 | + type: this.utils.editWidgetInfo.type, | |
102 | + sizeX: this.utils.editWidgetInfo.sizeX, | |
103 | + sizeY: this.utils.editWidgetInfo.sizeY, | |
104 | + resources: this.utils.editWidgetInfo.resources, | |
105 | + templateHtml: this.utils.editWidgetInfo.templateHtml, | |
106 | + templateCss: this.utils.editWidgetInfo.templateCss, | |
107 | + controllerScript: this.utils.editWidgetInfo.controllerScript, | |
108 | + settingsSchema: this.utils.editWidgetInfo.settingsSchema, | |
109 | + dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, | |
110 | + defaultConfig: this.utils.editWidgetInfo.defaultConfig | |
111 | + }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle' | |
112 | + ); | |
113 | + } | |
80 | 114 | const initSubject = new ReplaySubject(); |
81 | 115 | this.init$ = initSubject.asObservable(); |
82 | 116 | const loadDefaultWidgetInfoTasks = [ |
... | ... | @@ -110,7 +144,7 @@ export class WidgetComponentService { |
110 | 144 | widgetInfoSubject.complete(); |
111 | 145 | } else { |
112 | 146 | if (this.utils.widgetEditMode) { |
113 | - // TODO: | |
147 | + this.loadWidget(this.editingWidgetType, bundleAlias, isSystem, widgetInfoSubject); | |
114 | 148 | } else { |
115 | 149 | const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); |
116 | 150 | let fetchQueue = this.widgetsInfoFetchQueue.get(key); |
... | ... | @@ -377,4 +411,17 @@ export class WidgetComponentService { |
377 | 411 | this.widgetsInfoInMemoryCache.set(key, widgetInfo); |
378 | 412 | } |
379 | 413 | |
414 | + private deleteWidgetInfoFromCache(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean) { | |
415 | + const key = this.createWidgetInfoCacheKey(bundleAlias, widgetTypeAlias, isSystem); | |
416 | + this.widgetsInfoInMemoryCache.delete(key); | |
417 | + } | |
418 | + | |
419 | + private deleteWidgetsBundleFromCache(bundleAlias: string, isSystem: boolean) { | |
420 | + const key = (isSystem ? 'sys_' : '') + bundleAlias; | |
421 | + this.widgetsInfoInMemoryCache.forEach((widgetInfo, cacheKey) => { | |
422 | + if (cacheKey.startsWith(key)) { | |
423 | + this.widgetsInfoInMemoryCache.delete(cacheKey); | |
424 | + } | |
425 | + }); | |
426 | + } | |
380 | 427 | } | ... | ... |
... | ... | @@ -847,13 +847,16 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
847 | 847 | createSubscriptionSubject.error(null); |
848 | 848 | } |
849 | 849 | ); |
850 | + this.cd.detectChanges(); | |
850 | 851 | } else if (this.widget.type === widgetType.static) { |
851 | 852 | this.loadingData = false; |
852 | 853 | createSubscriptionSubject.next(); |
853 | 854 | createSubscriptionSubject.complete(); |
855 | + this.cd.detectChanges(); | |
854 | 856 | } else { |
855 | 857 | createSubscriptionSubject.next(); |
856 | 858 | createSubscriptionSubject.complete(); |
859 | + this.cd.detectChanges(); | |
857 | 860 | } |
858 | 861 | return createSubscriptionSubject.asObservable(); |
859 | 862 | } | ... | ... |
... | ... | @@ -33,7 +33,7 @@ export interface WidgetsData { |
33 | 33 | export interface DashboardCallbacks { |
34 | 34 | onEditWidget?: ($event: Event, widget: Widget) => void; |
35 | 35 | onExportWidget?: ($event: Event, widget: Widget) => void; |
36 | - onRemoveWidget?: ($event: Event, widget: Widget) => void; | |
36 | + onRemoveWidget?: ($event: Event, widget: Widget) => Observable<boolean>; | |
37 | 37 | onWidgetMouseDown?: ($event: Event, widget: Widget) => void; |
38 | 38 | onWidgetClicked?: ($event: Event, widget: Widget) => void; |
39 | 39 | prepareDashboardContextMenu?: ($event: Event) => void; | ... | ... |
... | ... | @@ -43,6 +43,8 @@ import { |
43 | 43 | import { ComponentFactory } from '@angular/core'; |
44 | 44 | import { HttpErrorResponse } from '@angular/common/http'; |
45 | 45 | import { RafService } from '@core/services/raf.service'; |
46 | +import { WidgetTypeId } from '@shared/models/id/widget-type-id'; | |
47 | +import { TenantId } from '@shared/models/id/tenant-id'; | |
46 | 48 | |
47 | 49 | export interface IWidgetAction { |
48 | 50 | name: string; |
... | ... | @@ -192,3 +194,25 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { |
192 | 194 | }; |
193 | 195 | } |
194 | 196 | |
197 | +export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, bundleAlias: string): WidgetType { | |
198 | + const descriptor: WidgetTypeDescriptor = { | |
199 | + type: widgetInfo.type, | |
200 | + sizeX: widgetInfo.sizeX, | |
201 | + sizeY: widgetInfo.sizeY, | |
202 | + resources: widgetInfo.resources, | |
203 | + templateHtml: widgetInfo.templateHtml, | |
204 | + templateCss: widgetInfo.templateCss, | |
205 | + controllerScript: widgetInfo.controllerScript, | |
206 | + settingsSchema: widgetInfo.settingsSchema, | |
207 | + dataKeySettingsSchema: widgetInfo.dataKeySettingsSchema, | |
208 | + defaultConfig: widgetInfo.defaultConfig | |
209 | + }; | |
210 | + return { | |
211 | + id, | |
212 | + tenantId, | |
213 | + bundleAlias, | |
214 | + alias: widgetInfo.alias, | |
215 | + name: widgetInfo.widgetName, | |
216 | + descriptor | |
217 | + }; | |
218 | +} | ... | ... |
... | ... | @@ -51,6 +51,7 @@ import { Subscription } from 'rxjs'; |
51 | 51 | import { FooterFabButtons } from '@shared/components/footer-fab-buttons.component'; |
52 | 52 | import { IStateController } from '@core/api/widget-api.models'; |
53 | 53 | import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; |
54 | +import { DashboardService } from '@core/http/dashboard.service'; | |
54 | 55 | |
55 | 56 | @Component({ |
56 | 57 | selector: 'tb-dashboard-page', |
... | ... | @@ -173,7 +174,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
173 | 174 | private dashboardUtils: DashboardUtilsService, |
174 | 175 | private authService: AuthService, |
175 | 176 | private entityService: EntityService, |
176 | - private dialogService: DialogService) { | |
177 | + private dialogService: DialogService, | |
178 | + private dashboardService: DashboardService) { | |
177 | 179 | super(store); |
178 | 180 | |
179 | 181 | this.rxSubscriptions.push(this.route.data.subscribe( |
... | ... | @@ -460,6 +462,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
460 | 462 | this.setEditMode(!this.isEdit, true); |
461 | 463 | } |
462 | 464 | |
465 | + public saveDashboard() { | |
466 | + this.setEditMode(false, false); | |
467 | + this.notifyDashboardUpdated(); | |
468 | + } | |
469 | + | |
463 | 470 | public openDashboardState(state: string, openRightLayout: boolean) { |
464 | 471 | const layoutsData = this.dashboardUtils.getStateLayoutsData(this.dashboard, state); |
465 | 472 | if (layoutsData) { |
... | ... | @@ -514,8 +521,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
514 | 521 | private setEditMode(isEdit: boolean, revert: boolean) { |
515 | 522 | this.isEdit = isEdit; |
516 | 523 | if (this.isEdit) { |
517 | - // TODO: | |
518 | - // this.dashboardCtx.stateController.preserveState(); | |
524 | + this.dashboardCtx.stateController.preserveState(); | |
519 | 525 | this.prevDashboard = deepClone(this.dashboard); |
520 | 526 | } else { |
521 | 527 | if (this.widgetEditMode) { |
... | ... | @@ -549,4 +555,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
549 | 555 | private entityAliasesUpdated() { |
550 | 556 | this.dashboardCtx.aliasController.updateEntityAliases(this.dashboard.configuration.entityAliases); |
551 | 557 | } |
558 | + | |
559 | + private notifyDashboardUpdated() { | |
560 | + if (this.widgetEditMode) { | |
561 | + const widget = this.layouts.main.layoutCtx.widgets[0]; | |
562 | + const layout = this.layouts.main.layoutCtx.widgetLayouts[widget.id]; | |
563 | + widget.sizeX = layout.sizeX; | |
564 | + widget.sizeY = layout.sizeY; | |
565 | + const message: WindowMessage = { | |
566 | + type: 'widgetEditUpdated', | |
567 | + data: widget | |
568 | + }; | |
569 | + this.window.parent.postMessage(JSON.stringify(message), '*'); | |
570 | + } else { | |
571 | + this.dashboardService.saveDashboard(this.dashboard); | |
572 | + } | |
573 | + } | |
552 | 574 | } | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<form #saveWidgetTypeAsForm="ngForm" | |
19 | + [formGroup]="saveWidgetTypeAsFormGroup"(ngSubmit)="saveAs()"> | |
20 | + <mat-toolbar fxLayout="row" color="primary"> | |
21 | + <h2 translate>widget.save-widget-type-as</h2> | |
22 | + <span fxFlex></span> | |
23 | + <button mat-button mat-icon-button | |
24 | + (click)="cancel()" | |
25 | + type="button"> | |
26 | + <mat-icon class="material-icons">close</mat-icon> | |
27 | + </button> | |
28 | + </mat-toolbar> | |
29 | + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> | |
30 | + </mat-progress-bar> | |
31 | + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> | |
32 | + <div mat-dialog-content> | |
33 | + <fieldset> | |
34 | + <span translate>widget.save-widget-type-as-text</span> | |
35 | + <mat-form-field class="mat-block"> | |
36 | + <mat-label translate>widget.title</mat-label> | |
37 | + <input matInput formControlName="title" required> | |
38 | + <mat-error *ngIf="saveWidgetTypeAsFormGroup.get('title').hasError('required')"> | |
39 | + {{ 'widget.title-required' | translate }} | |
40 | + </mat-error> | |
41 | + </mat-form-field> | |
42 | + <tb-widgets-bundle-select fxFlex | |
43 | + formControlName="widgetsBundle" | |
44 | + required | |
45 | + bundlesScope="{{bundlesScope}}"> | |
46 | + </tb-widgets-bundle-select> | |
47 | + </fieldset> | |
48 | + </div> | |
49 | + <div mat-dialog-actions fxLayout="row"> | |
50 | + <span fxFlex></span> | |
51 | + <button mat-button mat-raised-button color="primary" | |
52 | + type="submit" | |
53 | + [disabled]="(isLoading$ | async) || saveWidgetTypeAsForm.invalid | |
54 | + || !saveWidgetTypeAsForm.dirty"> | |
55 | + {{ 'action.saveAs' | translate }} | |
56 | + </button> | |
57 | + <button mat-button color="primary" | |
58 | + style="margin-right: 20px;" | |
59 | + type="button" | |
60 | + [disabled]="(isLoading$ | async)" | |
61 | + (click)="cancel()" cdkFocusInitial> | |
62 | + {{ 'action.cancel' | translate }} | |
63 | + </button> | |
64 | + </div> | |
65 | +</form> | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 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 { Component, OnInit } from '@angular/core'; | |
18 | +import { MatDialogRef } from '@angular/material'; | |
19 | +import { Store } from '@ngrx/store'; | |
20 | +import { AppState } from '@core/core.state'; | |
21 | +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
22 | +import { DialogComponent } from '@shared/components/dialog.component'; | |
23 | +import { Router } from '@angular/router'; | |
24 | +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | |
25 | +import { getCurrentAuthUser } from '@core/auth/auth.selectors'; | |
26 | +import { Authority } from '@shared/models/authority.enum'; | |
27 | + | |
28 | +export interface SaveWidgetTypeAsDialogResult { | |
29 | + widgetName: string; | |
30 | + bundleId: string; | |
31 | + bundleAlias: string; | |
32 | +} | |
33 | + | |
34 | +@Component({ | |
35 | + selector: 'tb-save-widget-type-as-dialog', | |
36 | + templateUrl: './save-widget-type-as-dialog.component.html', | |
37 | + styleUrls: [] | |
38 | +}) | |
39 | +export class SaveWidgetTypeAsDialogComponent extends | |
40 | + DialogComponent<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult> implements OnInit { | |
41 | + | |
42 | + saveWidgetTypeAsFormGroup: FormGroup; | |
43 | + | |
44 | + bundlesScope: string; | |
45 | + | |
46 | + constructor(protected store: Store<AppState>, | |
47 | + protected router: Router, | |
48 | + public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>, | |
49 | + public fb: FormBuilder) { | |
50 | + super(store, router, dialogRef); | |
51 | + | |
52 | + const authUser = getCurrentAuthUser(store); | |
53 | + if (authUser.authority === Authority.TENANT_ADMIN) { | |
54 | + this.bundlesScope = 'tenant'; | |
55 | + } else { | |
56 | + this.bundlesScope = 'system'; | |
57 | + } | |
58 | + } | |
59 | + | |
60 | + ngOnInit(): void { | |
61 | + this.saveWidgetTypeAsFormGroup = this.fb.group({ | |
62 | + title: [null, [Validators.required]], | |
63 | + widgetsBundle: [null, [Validators.required]] | |
64 | + }); | |
65 | + } | |
66 | + | |
67 | + cancel(): void { | |
68 | + this.dialogRef.close(null); | |
69 | + } | |
70 | + | |
71 | + saveAs(): void { | |
72 | + const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value; | |
73 | + const widgetsBundle: WidgetsBundle = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value; | |
74 | + const result: SaveWidgetTypeAsDialogResult = { | |
75 | + widgetName, | |
76 | + bundleId: widgetsBundle.id.id, | |
77 | + bundleAlias: widgetsBundle.alias | |
78 | + }; | |
79 | + this.dialogRef.close(result); | |
80 | + } | |
81 | +} | ... | ... |
... | ... | @@ -19,9 +19,10 @@ |
19 | 19 | <div fxFlex fxLayout="column"> |
20 | 20 | <div fxFlex fxLayout="column" tb-fullscreen [fullscreen]="fullscreen"> |
21 | 21 | <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px"> |
22 | - <mat-form-field floatLabel="always" class="tb-widget-title"> | |
22 | + <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title"> | |
23 | 23 | <mat-label></mat-label> |
24 | - <input [disabled]="isReadOnly" matInput [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true" | |
24 | + <input [disabled]="isReadOnly" matInput required | |
25 | + [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true" | |
25 | 26 | placeholder="{{ 'widget.title' | translate }}"/> |
26 | 27 | </mat-form-field> |
27 | 28 | <mat-form-field> |
... | ... | @@ -238,14 +239,15 @@ |
238 | 239 | </div> |
239 | 240 | <div tb-fullscreen [fullscreen]="iFrameFullscreen" style="width: 100%; height: 100%;"> |
240 | 241 | <iframe #widgetIFrame frameborder="0" height="100%" width="100%"></iframe> |
241 | - <button mat-button mat-icon-button | |
242 | - class="tb-fullscreen-button-style" | |
243 | - style="position: absolute; top: 10px; left: 10px; bottom: initial;" | |
244 | - (click)="iFrameFullscreen = !iFrameFullscreen" | |
245 | - matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | |
246 | - matTooltipPosition="above"> | |
247 | - <mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | |
248 | - </button> | |
242 | + <div style="position: absolute; top: 10px; left: 10px; bottom: initial;"> | |
243 | + <button mat-button mat-icon-button | |
244 | + class="tb-fullscreen-button-style" | |
245 | + (click)="iFrameFullscreen = !iFrameFullscreen" | |
246 | + matTooltip="{{(iFrameFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" | |
247 | + matTooltipPosition="above"> | |
248 | + <mat-icon>{{ iFrameFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon> | |
249 | + </button> | |
250 | + </div> | |
249 | 251 | </div> |
250 | 252 | </div> |
251 | 253 | </div> | ... | ... |
... | ... | @@ -20,9 +20,9 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
20 | 20 | import { Store } from '@ngrx/store'; |
21 | 21 | import { AppState } from '@core/core.state'; |
22 | 22 | import { WidgetService } from '@core/http/widget.service'; |
23 | -import { WidgetInfo } from '@home/models/widget-component.models'; | |
23 | +import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; | |
24 | 24 | import { WidgetConfig, widgetType, WidgetType, widgetTypesData, Widget } from '@shared/models/widget.models'; |
25 | -import { ActivatedRoute } from '@angular/router'; | |
25 | +import { ActivatedRoute, Router } from '@angular/router'; | |
26 | 26 | import { deepClone } from '@core/utils'; |
27 | 27 | import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; |
28 | 28 | import { AuthUser } from '@shared/models/user.model'; |
... | ... | @@ -40,6 +40,13 @@ import { WindowMessage } from '@shared/models/window-message.model'; |
40 | 40 | import { ExceptionData } from '@shared/models/error.models'; |
41 | 41 | import Timeout = NodeJS.Timeout; |
42 | 42 | import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions'; |
43 | +import { MatDialog } from '@angular/material/dialog'; | |
44 | +import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; | |
45 | +import { | |
46 | + SaveWidgetTypeAsDialogComponent, | |
47 | + SaveWidgetTypeAsDialogResult | |
48 | +} from '@home/pages/widget/save-widget-type-as-dialog.component'; | |
49 | +import { Subscription } from 'rxjs'; | |
43 | 50 | |
44 | 51 | @Component({ |
45 | 52 | selector: 'tb-widget-editor', |
... | ... | @@ -131,25 +138,37 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
131 | 138 | |
132 | 139 | saveWidgetTimeout: Timeout; |
133 | 140 | |
141 | + private rxSubscriptions = new Array<Subscription>(); | |
142 | + | |
134 | 143 | constructor(protected store: Store<AppState>, |
135 | 144 | @Inject(WINDOW) private window: Window, |
136 | 145 | private route: ActivatedRoute, |
146 | + private router: Router, | |
137 | 147 | private widgetService: WidgetService, |
138 | 148 | private hotkeysService: HotkeysService, |
139 | 149 | private translate: TranslateService, |
140 | - private raf: RafService) { | |
150 | + private raf: RafService, | |
151 | + private dialog: MatDialog) { | |
141 | 152 | super(store); |
142 | 153 | |
143 | 154 | this.authUser = getCurrentAuthUser(store); |
144 | 155 | |
145 | - this.widgetsBundle = this.route.snapshot.data.widgetsBundle; | |
156 | + this.rxSubscriptions.push(this.route.data.subscribe( | |
157 | + (data) => { | |
158 | + this.init(data); | |
159 | + } | |
160 | + )); | |
161 | + } | |
162 | + | |
163 | + private init(data: any) { | |
164 | + this.widgetsBundle = data.widgetsBundle; | |
146 | 165 | if (this.authUser.authority === Authority.TENANT_ADMIN) { |
147 | 166 | this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; |
148 | 167 | } else { |
149 | 168 | this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; |
150 | 169 | } |
151 | - this.widgetType = this.route.snapshot.data.widgetEditorData.widgetType; | |
152 | - this.widget = this.route.snapshot.data.widgetEditorData.widget; | |
170 | + this.widgetType = data.widgetEditorData.widgetType; | |
171 | + this.widget = data.widgetEditorData.widget; | |
153 | 172 | if (this.widgetType) { |
154 | 173 | const config = JSON.parse(this.widget.defaultConfig); |
155 | 174 | this.widget.defaultConfig = JSON.stringify(config); |
... | ... | @@ -176,6 +195,10 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
176 | 195 | // @ts-ignore |
177 | 196 | removeResizeListener(resizeListener.element, resizeListener.resizeListener); |
178 | 197 | }); |
198 | + this.rxSubscriptions.forEach((subscription) => { | |
199 | + subscription.unsubscribe(); | |
200 | + }); | |
201 | + this.rxSubscriptions.length = 0; | |
179 | 202 | } |
180 | 203 | |
181 | 204 | private initHotKeys(): void { |
... | ... | @@ -448,13 +471,52 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe |
448 | 471 | } |
449 | 472 | |
450 | 473 | private commitSaveWidget() { |
451 | - // TODO: | |
452 | - this.saveWidgetPending = false; | |
474 | + const id = (this.widgetType && this.widgetType.id) ? this.widgetType.id : undefined; | |
475 | + this.widgetService.saveWidgetType(this.widget, id, this.widgetsBundle.alias).subscribe( | |
476 | + (widgetTypeInstance) => { | |
477 | + this.setWidgetType(widgetTypeInstance); | |
478 | + this.saveWidgetPending = false; | |
479 | + this.store.dispatch(new ActionNotificationShow( | |
480 | + {message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500})); | |
481 | + }, | |
482 | + () => { | |
483 | + this.saveWidgetPending = false; | |
484 | + } | |
485 | + ); | |
453 | 486 | } |
454 | 487 | |
455 | 488 | private commitSaveWidgetAs() { |
456 | - // TODO: | |
457 | - this.saveWidgetAsPending = false; | |
489 | + this.dialog.open<SaveWidgetTypeAsDialogComponent, any, | |
490 | + SaveWidgetTypeAsDialogResult>(SaveWidgetTypeAsDialogComponent, { | |
491 | + disableClose: true, | |
492 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] | |
493 | + }).afterClosed().subscribe( | |
494 | + (saveWidgetAsData) => { | |
495 | + if (saveWidgetAsData) { | |
496 | + this.widget.widgetName = saveWidgetAsData.widgetName; | |
497 | + this.widget.alias = undefined; | |
498 | + const config = JSON.parse(this.widget.defaultConfig); | |
499 | + config.title = this.widget.widgetName; | |
500 | + this.widget.defaultConfig = JSON.stringify(config); | |
501 | + this.isDirty = false; | |
502 | + this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias).subscribe( | |
503 | + (widgetTypeInstance) => { | |
504 | + this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeInstance.id.id}`); | |
505 | + } | |
506 | + ); | |
507 | + } | |
508 | + this.saveWidgetAsPending = false; | |
509 | + } | |
510 | + ); | |
511 | + } | |
512 | + | |
513 | + private setWidgetType(widgetTypeInstance: WidgetType) { | |
514 | + this.widgetType = widgetTypeInstance; | |
515 | + this.widget = toWidgetInfo(this.widgetType); | |
516 | + const config = JSON.parse(this.widget.defaultConfig); | |
517 | + this.widget.defaultConfig = JSON.stringify(config); | |
518 | + this.origWidget = deepClone(this.widget); | |
519 | + this.isDirty = false; | |
458 | 520 | } |
459 | 521 | |
460 | 522 | applyWidgetScript(): void { | ... | ... |
... | ... | @@ -73,7 +73,7 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> { |
73 | 73 | } |
74 | 74 | return result; |
75 | 75 | }); |
76 | - const widgetTypes = new Array<Widget>(types.length); | |
76 | + const widgetTypes = new Array<Widget>(); | |
77 | 77 | let top = 0; |
78 | 78 | const lastTop = [0, 0, 0]; |
79 | 79 | let col = 0; | ... | ... |
... | ... | @@ -15,7 +15,7 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<section [fxShow]="!(isLoading$ | async) && (widgetTypes$ | async)?.length === 0" fxLayoutAlign="center center" | |
18 | +<section [fxShow]="!(isLoading$ | async) && widgetsData.widgets.length === 0" fxLayoutAlign="center center" | |
19 | 19 | style="text-transform: uppercase; display: flex; z-index: 1;" |
20 | 20 | class="tb-absolute-fill"> |
21 | 21 | <button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)"> |
... | ... | @@ -27,7 +27,8 @@ |
27 | 27 | style="text-transform: uppercase; display: flex;" |
28 | 28 | class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> |
29 | 29 | </section> |
30 | -<tb-dashboard [aliasController]="aliasController" | |
30 | +<tb-dashboard #dashboard | |
31 | + [aliasController]="aliasController" | |
31 | 32 | [widgets]="widgetsData.widgets" |
32 | 33 | [widgetLayouts]="widgetsData.widgetLayouts" |
33 | 34 | [isEdit]="false" | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, OnInit } from '@angular/core'; | |
17 | +import { Component, OnInit, ViewChild } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { PageComponent } from '@shared/components/page.component'; |
... | ... | @@ -24,13 +24,13 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
24 | 24 | import { ActivatedRoute, Router } from '@angular/router'; |
25 | 25 | import { Authority } from '@shared/models/authority.enum'; |
26 | 26 | import { NULL_UUID } from '@shared/models/id/has-uuid'; |
27 | -import { Observable } from 'rxjs'; | |
27 | +import { Observable, of } from 'rxjs'; | |
28 | 28 | import { Widget, widgetType } from '@app/shared/models/widget.models'; |
29 | 29 | import { WidgetService } from '@core/http/widget.service'; |
30 | -import { map, share } from 'rxjs/operators'; | |
30 | +import { map, mergeMap, share } from 'rxjs/operators'; | |
31 | 31 | import { DialogService } from '@core/services/dialog.service'; |
32 | 32 | import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; |
33 | -import { DashboardCallbacks, WidgetsData } from '@home/models/dashboard-component.models'; | |
33 | +import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models'; | |
34 | 34 | import { IAliasController } from '@app/core/api/widget-api.models'; |
35 | 35 | import { toWidgetInfo } from '@home/models/widget-component.models'; |
36 | 36 | import { DummyAliasController } from '@core/api/alias-controller'; |
... | ... | @@ -41,6 +41,7 @@ import { |
41 | 41 | import { DeviceCredentials } from '@shared/models/device.models'; |
42 | 42 | import { MatDialog } from '@angular/material/dialog'; |
43 | 43 | import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; |
44 | +import { TranslateService } from '@ngx-translate/core'; | |
44 | 45 | |
45 | 46 | @Component({ |
46 | 47 | selector: 'tb-widget-library', |
... | ... | @@ -86,12 +87,15 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { |
86 | 87 | |
87 | 88 | aliasController: IAliasController = new DummyAliasController(); |
88 | 89 | |
90 | + @ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; | |
91 | + | |
89 | 92 | constructor(protected store: Store<AppState>, |
90 | 93 | private route: ActivatedRoute, |
91 | 94 | private router: Router, |
92 | 95 | private widgetService: WidgetService, |
93 | 96 | private dialogService: DialogService, |
94 | - private dialog: MatDialog) { | |
97 | + private dialog: MatDialog, | |
98 | + private translate: TranslateService) { | |
95 | 99 | super(store); |
96 | 100 | |
97 | 101 | this.authUser = getCurrentAuthUser(store); |
... | ... | @@ -146,11 +150,32 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { |
146 | 150 | this.dialogService.todo(); |
147 | 151 | } |
148 | 152 | |
149 | - removeWidgetType($event: Event, widget: Widget): void { | |
153 | + removeWidgetType($event: Event, widget: Widget): Observable<boolean> { | |
150 | 154 | if ($event) { |
151 | 155 | $event.stopPropagation(); |
152 | 156 | } |
153 | - this.dialogService.todo(); | |
157 | + return this.dialogService.confirm( | |
158 | + this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}), | |
159 | + this.translate.instant('widget.remove-widget-type-text'), | |
160 | + this.translate.instant('action.no'), | |
161 | + this.translate.instant('action.yes'), | |
162 | + ).pipe( | |
163 | + mergeMap((result) => { | |
164 | + if (result) { | |
165 | + return this.widgetService.deleteWidgetType(widget.bundleAlias, widget.typeAlias, widget.isSystemType); | |
166 | + } else { | |
167 | + return of(false); | |
168 | + } | |
169 | + }), | |
170 | + map((result) => { | |
171 | + if (result !== false) { | |
172 | + this.widgetsData.widgets.splice(this.widgetsData.widgets.indexOf(widget), 1); | |
173 | + return true; | |
174 | + } else { | |
175 | + return false; | |
176 | + } | |
177 | + } | |
178 | + )); | |
154 | 179 | } |
155 | 180 | |
156 | 181 | } | ... | ... |
... | ... | @@ -23,17 +23,20 @@ import {HomeComponentsModule} from '@modules/home/components/home-components.mod |
23 | 23 | import { WidgetLibraryComponent } from './widget-library.component'; |
24 | 24 | import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; |
25 | 25 | import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; |
26 | +import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component'; | |
26 | 27 | |
27 | 28 | @NgModule({ |
28 | 29 | entryComponents: [ |
29 | 30 | WidgetsBundleComponent, |
30 | - SelectWidgetTypeDialogComponent | |
31 | + SelectWidgetTypeDialogComponent, | |
32 | + SaveWidgetTypeAsDialogComponent | |
31 | 33 | ], |
32 | 34 | declarations: [ |
33 | 35 | WidgetsBundleComponent, |
34 | 36 | WidgetLibraryComponent, |
35 | 37 | WidgetEditorComponent, |
36 | - SelectWidgetTypeDialogComponent | |
38 | + SelectWidgetTypeDialogComponent, | |
39 | + SaveWidgetTypeAsDialogComponent | |
37 | 40 | ], |
38 | 41 | imports: [ |
39 | 42 | CommonModule, | ... | ... |
... | ... | @@ -38,6 +38,7 @@ import { |
38 | 38 | DashboardSelectPanelComponent, |
39 | 39 | DashboardSelectPanelData |
40 | 40 | } from './dashboard-select-panel.component'; |
41 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | |
41 | 42 | |
42 | 43 | @Component({ |
43 | 44 | selector: 'tb-dashboard-select', |
... | ... | @@ -200,7 +201,7 @@ export class DashboardSelectComponent implements ControlValueAccessor, OnInit { |
200 | 201 | let dashboardsObservable: Observable<PageData<DashboardInfo>>; |
201 | 202 | const authUser = getCurrentAuthUser(this.store); |
202 | 203 | if (this.dashboardsScope === 'customer' || authUser.authority === Authority.CUSTOMER_USER) { |
203 | - if (this.customerId) { | |
204 | + if (this.customerId && this.customerId !== NULL_UUID) { | |
204 | 205 | dashboardsObservable = this.dashboardService.getCustomerDashboards(this.customerId, pageLink, false, true); |
205 | 206 | } else { |
206 | 207 | dashboardsObservable = of(emptyPageData()); | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2019 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<mat-form-field floatLabel="always" hideRequiredMarker class="mat-block"> | |
19 | + <mat-label></mat-label> | |
20 | + <mat-select [required]="required" | |
21 | + [disabled]="disabled" | |
22 | + [(ngModel)]="widgetsBundle" | |
23 | + matInput | |
24 | + panelClass="tb-widgets-bundle-select" | |
25 | + placeholder="{{ 'widget.select-widgets-bundle' | translate }}" | |
26 | + (ngModelChange)="widgetsBundleChanged()"> | |
27 | + <mat-option *ngFor="let widgetsBundle of widgetsBundles$ | async" [value]="widgetsBundle"> | |
28 | + <div class="tb-bundle-item"> | |
29 | + <span>{{widgetsBundle.title}}</span> | |
30 | + <span translate class="tb-bundle-system" *ngIf="isSystem(item)">widgets-bundle.system</span> | |
31 | + </div> | |
32 | + </mat-option> | |
33 | + </mat-select> | |
34 | +</mat-form-field> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2019 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 | +tb-widgets-bundle-select { | |
18 | + mat-select { | |
19 | + margin: 0; | |
20 | + } | |
21 | + | |
22 | + .tb-bundle-item { | |
23 | + height: 24px; | |
24 | + line-height: 24px; | |
25 | + } | |
26 | +} | |
27 | + | |
28 | +.tb-widgets-bundle-select { | |
29 | + .tb-bundle-item { | |
30 | + height: 48px; | |
31 | + line-height: 48px; | |
32 | + } | |
33 | +} | |
34 | + | |
35 | +tb-widgets-bundle-select, | |
36 | +.tb-widgets-bundle-select { | |
37 | + .mat-select-value-text { | |
38 | + display: block; | |
39 | + width: 100%; | |
40 | + } | |
41 | + | |
42 | + .tb-bundle-item { | |
43 | + display: inline-block; | |
44 | + width: 100%; | |
45 | + | |
46 | + span { | |
47 | + display: inline-block; | |
48 | + vertical-align: middle; | |
49 | + } | |
50 | + | |
51 | + .tb-bundle-system { | |
52 | + float: right; | |
53 | + font-size: .8rem; | |
54 | + opacity: .8; | |
55 | + } | |
56 | + } | |
57 | + | |
58 | + mat-option { | |
59 | + height: auto !important; | |
60 | + white-space: normal !important; | |
61 | + } | |
62 | +} | |
63 | + | |
64 | +mat-toolbar { | |
65 | + tb-widgets-bundle-select { | |
66 | + mat-select { | |
67 | + background: rgba(255, 255, 255, .2); | |
68 | + padding: 5px 20px; | |
69 | + | |
70 | + .mat-select-value-text { | |
71 | + font-size: 1.2rem; | |
72 | + color: #fff; | |
73 | + | |
74 | + span:first-child::after { | |
75 | + color: #fff; | |
76 | + } | |
77 | + } | |
78 | + | |
79 | + .mat-select-value.mat-select-placeholder { | |
80 | + color: #fff; | |
81 | + opacity: .8; | |
82 | + } | |
83 | + } | |
84 | + | |
85 | + mat-select.ng-invalid.ng-touched { | |
86 | + .mat-select-value-text { | |
87 | + color: #fff !important; | |
88 | + } | |
89 | + } | |
90 | + } | |
91 | +} | ... | ... |
1 | +/// | |
2 | +/// Copyright © 2016-2019 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 { Component, forwardRef, Input, OnChanges, OnInit, ViewEncapsulation, SimpleChanges } from '@angular/core'; | |
18 | +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | |
19 | +import { Observable } from 'rxjs'; | |
20 | +import { share, tap } from 'rxjs/operators'; | |
21 | +import { Store } from '@ngrx/store'; | |
22 | +import { AppState } from '@app/core/core.state'; | |
23 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | |
24 | +import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; | |
25 | +import { WidgetService } from '@core/http/widget.service'; | |
26 | +import { isDefined } from '@core/utils'; | |
27 | +import { NULL_UUID } from '@shared/models/id/has-uuid'; | |
28 | + | |
29 | +@Component({ | |
30 | + selector: 'tb-widgets-bundle-select', | |
31 | + templateUrl: './widgets-bundle-select.component.html', | |
32 | + styleUrls: ['./widgets-bundle-select.component.scss'], | |
33 | + providers: [{ | |
34 | + provide: NG_VALUE_ACCESSOR, | |
35 | + useExisting: forwardRef(() => WidgetsBundleSelectComponent), | |
36 | + multi: true | |
37 | + }], | |
38 | + encapsulation: ViewEncapsulation.None | |
39 | +}) | |
40 | +export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnInit, OnChanges { | |
41 | + | |
42 | + @Input() | |
43 | + bundlesScope: 'system' | 'tenant'; | |
44 | + | |
45 | + @Input() | |
46 | + selectFirstBundle: boolean; | |
47 | + | |
48 | + @Input() | |
49 | + selectBundleAlias: string; | |
50 | + | |
51 | + private requiredValue: boolean; | |
52 | + get required(): boolean { | |
53 | + return this.requiredValue; | |
54 | + } | |
55 | + @Input() | |
56 | + set required(value: boolean) { | |
57 | + this.requiredValue = coerceBooleanProperty(value); | |
58 | + } | |
59 | + | |
60 | + @Input() | |
61 | + disabled: boolean; | |
62 | + | |
63 | + widgetsBundles$: Observable<Array<WidgetsBundle>>; | |
64 | + | |
65 | + widgetsBundles: Array<WidgetsBundle>; | |
66 | + | |
67 | + widgetsBundle: WidgetsBundle | null; | |
68 | + | |
69 | + private propagateChange = (v: any) => { }; | |
70 | + | |
71 | + constructor(private store: Store<AppState>, | |
72 | + private widgetService: WidgetService) { | |
73 | + } | |
74 | + | |
75 | + registerOnChange(fn: any): void { | |
76 | + this.propagateChange = fn; | |
77 | + } | |
78 | + | |
79 | + registerOnTouched(fn: any): void { | |
80 | + } | |
81 | + | |
82 | + ngOnInit() { | |
83 | + this.widgetsBundles$ = this.getWidgetsBundles().pipe( | |
84 | + tap((widgetsBundles) => { | |
85 | + this.widgetsBundles = widgetsBundles; | |
86 | + if (this.selectFirstBundle) { | |
87 | + if (widgetsBundles.length > 0) { | |
88 | + if (this.widgetsBundle !== widgetsBundles[0]) { | |
89 | + this.widgetsBundle = widgetsBundles[0]; | |
90 | + this.updateView(); | |
91 | + } else if (isDefined(this.selectBundleAlias)) { | |
92 | + this.selectWidgetsBundleByAlias(this.selectBundleAlias); | |
93 | + } | |
94 | + } | |
95 | + } | |
96 | + }), | |
97 | + share() | |
98 | + ); | |
99 | + } | |
100 | + | |
101 | + ngOnChanges(changes: SimpleChanges): void { | |
102 | + for (const propName of Object.keys(changes)) { | |
103 | + const change = changes[propName]; | |
104 | + if (!change.firstChange && change.currentValue !== change.previousValue) { | |
105 | + if (propName === 'selectBundleAlias') { | |
106 | + this.selectWidgetsBundleByAlias(this.selectBundleAlias); | |
107 | + } | |
108 | + } | |
109 | + } | |
110 | + } | |
111 | + | |
112 | + setDisabledState(isDisabled: boolean): void { | |
113 | + this.disabled = isDisabled; | |
114 | + } | |
115 | + | |
116 | + writeValue(value: WidgetsBundle | null): void { | |
117 | + this.widgetsBundle = value; | |
118 | + } | |
119 | + | |
120 | + widgetsBundleChanged() { | |
121 | + this.updateView(); | |
122 | + } | |
123 | + | |
124 | + isSystem(item: WidgetsBundle) { | |
125 | + return item && item.tenantId.id === NULL_UUID; | |
126 | + } | |
127 | + | |
128 | + private selectWidgetsBundleByAlias(alias: string) { | |
129 | + if (this.widgetsBundles && alias) { | |
130 | + const found = this.widgetsBundles.find((widgetsBundle) => widgetsBundle.alias === alias); | |
131 | + if (found && this.widgetsBundle !== found) { | |
132 | + this.widgetsBundle = found; | |
133 | + this.updateView(); | |
134 | + } | |
135 | + } | |
136 | + } | |
137 | + | |
138 | + private updateView() { | |
139 | + this.propagateChange(this.widgetsBundle); | |
140 | + } | |
141 | + | |
142 | + private getWidgetsBundles(): Observable<Array<WidgetsBundle>> { | |
143 | + let widgetsBundlesObservable: Observable<Array<WidgetsBundle>>; | |
144 | + if (this.bundlesScope) { | |
145 | + if (this.bundlesScope === 'system') { | |
146 | + widgetsBundlesObservable = this.widgetService.getSystemWidgetsBundles(); | |
147 | + } else if (this.bundlesScope === 'tenant') { | |
148 | + widgetsBundlesObservable = this.widgetService.getTenantWidgetsBundles(); | |
149 | + } | |
150 | + } else { | |
151 | + widgetsBundlesObservable = this.widgetService.getAllWidgetsBundles(); | |
152 | + } | |
153 | + return widgetsBundlesObservable; | |
154 | + } | |
155 | + | |
156 | +} | ... | ... |
... | ... | @@ -94,6 +94,7 @@ import { MatSpinner } from '@angular/material/progress-spinner'; |
94 | 94 | import { FabToolbarComponent, FabActionsDirective, FabTriggerDirective } from './components/fab-toolbar.component'; |
95 | 95 | import { DashboardSelectPanelComponent } from '@shared/components/dashboard-select-panel.component'; |
96 | 96 | import { DashboardSelectComponent } from '@shared/components/dashboard-select.component'; |
97 | +import { WidgetsBundleSelectComponent } from './components/widgets-bundle-select.component'; | |
97 | 98 | |
98 | 99 | @NgModule({ |
99 | 100 | providers: [ |
... | ... | @@ -145,6 +146,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co |
145 | 146 | FabTriggerDirective, |
146 | 147 | FabActionsDirective, |
147 | 148 | FabToolbarComponent, |
149 | + WidgetsBundleSelectComponent, | |
148 | 150 | NospacePipe, |
149 | 151 | MillisecondsToTimeStringPipe, |
150 | 152 | EnumToArrayPipe, |
... | ... | @@ -226,6 +228,7 @@ import { DashboardSelectComponent } from '@shared/components/dashboard-select.co |
226 | 228 | FabTriggerDirective, |
227 | 229 | FabActionsDirective, |
228 | 230 | FabToolbarComponent, |
231 | + WidgetsBundleSelectComponent, | |
229 | 232 | ValueInputComponent, |
230 | 233 | MatButtonModule, |
231 | 234 | MatCheckboxModule, | ... | ... |