52 changed files with 1986 additions and 727 deletions
@ -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-toolbar color="primary"> |
|||
<h2 translate>widgets-bundle.export</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div mat-dialog-content> |
|||
<fieldset [disabled]="isLoading$ | async"> |
|||
<mat-checkbox [formControl]="exportWidgetsFormControl">{{ 'widgets-bundle.export-widgets-bundle-widgets-prompt' | translate }}</mat-checkbox> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()"> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
(click)="export()" |
|||
[disabled]="(isLoading$ | async)"> |
|||
{{ 'action.export' | translate }} |
|||
</button> |
|||
</div> |
|||
@ -0,0 +1,67 @@ |
|||
///
|
|||
/// 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, Inject, OnInit } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { FormControl } from '@angular/forms'; |
|||
import { Router } from '@angular/router'; |
|||
import { DialogComponent } from '@app/shared/components/dialog.component'; |
|||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
|||
|
|||
export interface ExportWidgetsBundleDialogData { |
|||
widgetsBundle: WidgetsBundle; |
|||
} |
|||
|
|||
export interface ExportWidgetsBundleDialogResult { |
|||
exportWidgets: boolean; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-export-widgets-bundle-dialog', |
|||
templateUrl: './export-widgets-bundle-dialog.component.html', |
|||
providers: [], |
|||
styleUrls: [] |
|||
}) |
|||
export class ExportWidgetsBundleDialogComponent extends DialogComponent<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogResult> |
|||
implements OnInit { |
|||
|
|||
widgetsBundle: WidgetsBundle; |
|||
|
|||
exportWidgetsFormControl = new FormControl(false); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: ExportWidgetsBundleDialogData, |
|||
public dialogRef: MatDialogRef<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogResult>) { |
|||
super(store, router, dialogRef); |
|||
this.widgetsBundle = data.widgetsBundle; |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
export(): void { |
|||
this.dialogRef.close({ |
|||
exportWidgets: this.exportWidgetsFormControl.value |
|||
}); |
|||
} |
|||
} |
|||
@ -1,56 +0,0 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<form [formGroup]="moveWidgetTypeFormGroup" (ngSubmit)="move()"> |
|||
<mat-toolbar color="primary"> |
|||
<h2 translate>widget.move-widget-type</h2> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
(click)="cancel()" |
|||
type="button"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
</mat-toolbar> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> |
|||
<div mat-dialog-content> |
|||
<fieldset> |
|||
<span translate>widget.move-widget-type-text</span> |
|||
<tb-widgets-bundle-select fxFlex |
|||
formControlName="widgetsBundle" |
|||
required |
|||
[excludeBundleIds]="[data.currentBundleId]" |
|||
bundlesScope="{{bundlesScope}}"> |
|||
</tb-widgets-bundle-select> |
|||
</fieldset> |
|||
</div> |
|||
<div mat-dialog-actions fxLayoutAlign="end center"> |
|||
<button mat-button color="primary" |
|||
type="button" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()" cdkFocusInitial> |
|||
{{ 'action.cancel' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
type="submit" |
|||
[disabled]="(isLoading$ | async) || moveWidgetTypeFormGroup.invalid |
|||
|| !moveWidgetTypeFormGroup.dirty"> |
|||
{{ 'action.move' | translate }} |
|||
</button> |
|||
</div> |
|||
</form> |
|||
@ -1,82 +0,0 @@ |
|||
///
|
|||
/// 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, Inject, OnInit } from '@angular/core'; |
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { DialogComponent } from '@shared/components/dialog.component'; |
|||
import { Router } from '@angular/router'; |
|||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
|
|||
export interface MoveWidgetTypeDialogResult { |
|||
bundleId: string; |
|||
bundleAlias: string; |
|||
} |
|||
|
|||
export interface MoveWidgetTypeDialogData { |
|||
currentBundleId: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-move-widget-type-dialog', |
|||
templateUrl: './move-widget-type-dialog.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class MoveWidgetTypeDialogComponent extends |
|||
DialogComponent<MoveWidgetTypeDialogComponent, MoveWidgetTypeDialogResult> implements OnInit { |
|||
|
|||
moveWidgetTypeFormGroup: UntypedFormGroup; |
|||
|
|||
bundlesScope: string; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
protected router: Router, |
|||
@Inject(MAT_DIALOG_DATA) public data: MoveWidgetTypeDialogData, |
|||
public dialogRef: MatDialogRef<MoveWidgetTypeDialogComponent, MoveWidgetTypeDialogResult>, |
|||
public fb: UntypedFormBuilder) { |
|||
super(store, router, dialogRef); |
|||
|
|||
const authUser = getCurrentAuthUser(store); |
|||
if (authUser.authority === Authority.TENANT_ADMIN) { |
|||
this.bundlesScope = 'tenant'; |
|||
} else { |
|||
this.bundlesScope = 'system'; |
|||
} |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.moveWidgetTypeFormGroup = this.fb.group({ |
|||
widgetsBundle: [null, [Validators.required]] |
|||
}); |
|||
} |
|||
|
|||
cancel(): void { |
|||
this.dialogRef.close(null); |
|||
} |
|||
|
|||
move(): void { |
|||
const widgetsBundle: WidgetsBundle = this.moveWidgetTypeFormGroup.get('widgetsBundle').value; |
|||
const result: MoveWidgetTypeDialogResult = { |
|||
bundleId: widgetsBundle.id.id, |
|||
bundleAlias: widgetsBundle.alias |
|||
}; |
|||
this.dialogRef.close(result); |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<section [fxShow]="!(isLoading$ | async) && widgetsData.widgets.length === 0" fxLayoutAlign="center center" |
|||
style="display: flex; z-index: 1;" |
|||
class="tb-absolute-fill"> |
|||
<button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)"> |
|||
<mat-icon class="tb-mat-96">add</mat-icon> |
|||
{{ 'widget.add-widget-type' | translate }} |
|||
</button> |
|||
<span translate *ngIf="isReadOnly" |
|||
fxLayoutAlign="center center" |
|||
style="display: flex;" |
|||
class="mat-headline-5 tb-absolute-fill">widgets-bundle.empty</span> |
|||
</section> |
|||
<tb-dashboard class="tb-widget-library" |
|||
#dashboard |
|||
[aliasController]="aliasController" |
|||
[widgets]="widgetsData.widgets" |
|||
[widgetLayouts]="widgetsData.widgetLayouts" |
|||
[isEdit]="false" |
|||
[isEditActionEnabled]="true" |
|||
[isExportActionEnabled]="true" |
|||
[isRemoveActionEnabled]="!isReadOnly" |
|||
[disableWidgetInteraction]="true" |
|||
[callbacks]="dashboardCallbacks"></tb-dashboard> |
|||
<tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons"> |
|||
</tb-footer-fab-buttons> |
|||
@ -1,205 +0,0 @@ |
|||
///
|
|||
/// 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, OnInit, ViewChild } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { AuthUser } from '@shared/models/user.model'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
|||
import { ActivatedRoute, Router } from '@angular/router'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
import { of } from 'rxjs'; |
|||
import { Widget, widgetType } from '@app/shared/models/widget.models'; |
|||
import { WidgetService } from '@core/http/widget.service'; |
|||
import { map, mergeMap } from 'rxjs/operators'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component'; |
|||
import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models'; |
|||
import { IAliasController, IStateController, StateParams } from '@app/core/api/widget-api.models'; |
|||
import { AliasController } from '@core/api/alias-controller'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { ImportExportService } from '@home/components/import-export/import-export.service'; |
|||
import { UtilsService } from '@core/services/utils.service'; |
|||
import { EntityService } from '@core/http/entity.service'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-library', |
|||
templateUrl: './widget-library.component.html', |
|||
styleUrls: ['./widget-library.component.scss'] |
|||
}) |
|||
export class WidgetLibraryComponent extends PageComponent implements OnInit { |
|||
|
|||
authUser: AuthUser; |
|||
|
|||
isReadOnly: boolean; |
|||
|
|||
widgetsBundle: WidgetsBundle; |
|||
|
|||
widgetsData: WidgetsData; |
|||
|
|||
footerFabButtons: FooterFabButtons = { |
|||
fabTogglerName: 'widget.add-widget-type', |
|||
fabTogglerIcon: 'add', |
|||
buttons: [ |
|||
{ |
|||
name: 'widget-type.create-new-widget-type', |
|||
icon: 'insert_drive_file', |
|||
onAction: ($event) => { |
|||
this.addWidgetType($event); |
|||
} |
|||
}, |
|||
{ |
|||
name: 'widget-type.import', |
|||
icon: 'file_upload', |
|||
onAction: ($event) => { |
|||
this.importWidgetType($event); |
|||
} |
|||
} |
|||
] |
|||
}; |
|||
|
|||
dashboardCallbacks: DashboardCallbacks = { |
|||
onEditWidget: this.openWidgetType.bind(this), |
|||
onWidgetClicked: this.openWidgetType.bind(this), |
|||
onExportWidget: this.exportWidgetType.bind(this), |
|||
onRemoveWidget: this.removeWidgetType.bind(this) |
|||
}; |
|||
|
|||
aliasController: IAliasController = new AliasController(this.utils, |
|||
this.entityService, |
|||
this.translate, |
|||
() => ({ |
|||
getStateParams: (): StateParams => ({}) |
|||
} as IStateController), |
|||
{}, |
|||
{}); |
|||
|
|||
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent; |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private route: ActivatedRoute, |
|||
private router: Router, |
|||
private widgetService: WidgetService, |
|||
private dialogService: DialogService, |
|||
private importExport: ImportExportService, |
|||
private dialog: MatDialog, |
|||
private translate: TranslateService, |
|||
private utils: UtilsService, |
|||
private entityService: EntityService) { |
|||
super(store); |
|||
|
|||
this.authUser = getCurrentAuthUser(this.store); |
|||
this.widgetsBundle = this.route.snapshot.data.widgetsBundle; |
|||
this.widgetsData = this.route.snapshot.data.widgetsData; |
|||
if (this.authUser.authority === Authority.TENANT_ADMIN) { |
|||
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; |
|||
} else { |
|||
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; |
|||
} |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
addWidgetType($event: Event): void { |
|||
this.openWidgetType($event); |
|||
} |
|||
|
|||
importWidgetType($event: Event): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.importExport.importWidgetType(this.widgetsBundle.alias).subscribe( |
|||
(widgetTypeInstance) => { |
|||
if (widgetTypeInstance) { |
|||
this.reload(); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
private reload() { |
|||
const bundleAlias = this.widgetsBundle.alias; |
|||
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; |
|||
this.widgetService.loadBundleLibraryWidgets(bundleAlias, isSystem).subscribe( |
|||
(widgets) => { |
|||
this.widgetsData = {widgets}; |
|||
} |
|||
); |
|||
} |
|||
|
|||
openWidgetType($event: Event, widget?: Widget): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
if (widget) { |
|||
this.router.navigate([widget.typeId.id], {relativeTo: this.route}); |
|||
} else { |
|||
this.dialog.open<SelectWidgetTypeDialogComponent, any, |
|||
widgetType>(SelectWidgetTypeDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
|||
}).afterClosed().subscribe( |
|||
(type) => { |
|||
if (type) { |
|||
this.router.navigate([type], {relativeTo: this.route}); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
} |
|||
|
|||
exportWidgetType($event: Event, widget: Widget): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.importExport.exportWidgetType(widget.typeId.id); |
|||
} |
|||
|
|||
removeWidgetType($event: Event, widget: Widget): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialogService.confirm( |
|||
this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}), |
|||
this.translate.instant('widget.remove-widget-type-text'), |
|||
this.translate.instant('action.no'), |
|||
this.translate.instant('action.yes'), |
|||
).pipe( |
|||
mergeMap((result) => { |
|||
if (result) { |
|||
return this.widgetService.deleteWidgetType(widget.typeFullFqn); |
|||
} else { |
|||
return of(false); |
|||
} |
|||
}), |
|||
map((result) => { |
|||
if (result !== false) { |
|||
this.reload(); |
|||
return true; |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
)).subscribe(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
<!-- |
|||
|
|||
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]="selectWidgetTypeFormGroup" class="mat-block" [floatLabel]="floatLabel" |
|||
[appearance]="appearance" [subscriptSizing]="subscriptSizing"> |
|||
<mat-label *ngIf="label">{{ label }}</mat-label> |
|||
<input matInput type="text" |
|||
#widgetTypeInput |
|||
placeholder="{{ placeholder }}" |
|||
formControlName="widgetType" |
|||
(focusin)="onFocus()" |
|||
[required]="required" |
|||
[matAutocomplete]="widgetTypeAutocomplete"> |
|||
<button *ngIf="selectWidgetTypeFormGroup.get('widgetType').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 tb-widget-type-autocomplete" |
|||
#widgetTypeAutocomplete |
|||
[displayWith]="displayWidgetTypeFn"> |
|||
<mat-option *ngFor="let widgetType of filteredWidgetTypes | async" [value]="widgetType"> |
|||
<div class="tb-widget-type-option-container"> |
|||
<img class="tb-widget-type-option-image-preview" [src]="getPreviewImage(widgetType.image)" alt="{{ widgetType.name }}"> |
|||
<div class="tb-widget-type-option-details"> |
|||
<div class="tb-widget-type-option-text" [innerHTML]="widgetType.name | highlight:searchText:true"></div> |
|||
<div *ngIf="widgetType.deprecated" class="tb-widget-type-option-deprecated">{{ 'widget.deprecated' | translate }}</div> |
|||
</div> |
|||
</div> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredWidgetTypes | async)?.length" [value]="null"> |
|||
<span> |
|||
{{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }} |
|||
</span> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-error> |
|||
<ng-content select="[tb-error]"></ng-content> |
|||
</mat-error> |
|||
<mat-hint> |
|||
<ng-content select="[tb-hint]"></ng-content> |
|||
</mat-hint> |
|||
</mat-form-field> |
|||
@ -0,0 +1,49 @@ |
|||
/** |
|||
* 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. |
|||
*/ |
|||
.tb-widget-type-autocomplete { |
|||
.mat-mdc-option { |
|||
.mdc-list-item__primary-text { |
|||
width: 100%; |
|||
.tb-widget-type-option-container { |
|||
display: flex; |
|||
gap: 8px; |
|||
align-items: center; |
|||
} |
|||
.tb-widget-type-option-image-preview { |
|||
width: 36px; |
|||
max-height: 100%; |
|||
object-fit: contain; |
|||
border-radius: 6px; |
|||
} |
|||
.tb-widget-type-option-details { |
|||
flex: 1; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
.tb-widget-type-option-text { |
|||
font-size: 14px; |
|||
font-weight: 400; |
|||
letter-spacing: 0.2px; |
|||
} |
|||
.tb-widget-type-option-deprecated { |
|||
font-size: 13px; |
|||
font-weight: 400; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,227 @@ |
|||
///
|
|||
/// 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 { |
|||
AfterViewInit, |
|||
Component, |
|||
ElementRef, |
|||
forwardRef, |
|||
Input, |
|||
OnInit, |
|||
ViewChild, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { PageLink } from '@shared/models/page/page-link'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; |
|||
import { emptyPageData } from '@shared/models/page/page-data'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@app/core/core.state'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field'; |
|||
import { WidgetTypeInfo } from '@shared/models/widget.models'; |
|||
import { coerceBoolean } from '@shared/decorators/coercion'; |
|||
import { WidgetService } from '@core/http/widget.service'; |
|||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-type-autocomplete', |
|||
templateUrl: './widget-type-autocomplete.component.html', |
|||
styleUrls: ['./widget-type-autocomplete.component.scss'], |
|||
providers: [{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => WidgetTypeAutocompleteComponent), |
|||
multi: true |
|||
}], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class WidgetTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { |
|||
|
|||
private dirty = false; |
|||
|
|||
selectWidgetTypeFormGroup: UntypedFormGroup; |
|||
|
|||
modelValue: WidgetTypeInfo | null; |
|||
|
|||
@Input() |
|||
label = this.translate.instant('widget.widget'); |
|||
|
|||
@Input() |
|||
placeholder: string; |
|||
|
|||
@Input() |
|||
floatLabel: FloatLabelType = 'auto'; |
|||
|
|||
@Input() |
|||
appearance: MatFormFieldAppearance = 'fill'; |
|||
|
|||
@Input() |
|||
subscriptSizing: SubscriptSizing = 'fixed'; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
required: boolean; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
excludeWidgetTypeIds: Array<string>; |
|||
|
|||
@ViewChild('widgetTypeInput', {static: true}) widgetTypeInput: ElementRef; |
|||
|
|||
filteredWidgetTypes: Observable<Array<WidgetTypeInfo>>; |
|||
|
|||
searchText = ''; |
|||
|
|||
private propagateChange = (v: any) => { }; |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
public translate: TranslateService, |
|||
private widgetService: WidgetService, |
|||
private sanitizer: DomSanitizer, |
|||
private fb: UntypedFormBuilder) { |
|||
this.selectWidgetTypeFormGroup = this.fb.group({ |
|||
widgetType: [null] |
|||
}); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
this.filteredWidgetTypes = this.selectWidgetTypeFormGroup.get('widgetType').valueChanges |
|||
.pipe( |
|||
debounceTime(150), |
|||
tap(value => { |
|||
let modelValue; |
|||
if (typeof value === 'string' || !value) { |
|||
modelValue = null; |
|||
} else { |
|||
modelValue = value; |
|||
} |
|||
this.updateView(modelValue); |
|||
}), |
|||
map(value => value ? (typeof value === 'string' ? value : value.name) : ''), |
|||
distinctUntilChanged(), |
|||
switchMap(name => this.fetchWidgetTypes(name) ), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
ngAfterViewInit(): void { |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (this.disabled) { |
|||
this.selectWidgetTypeFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.selectWidgetTypeFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
writeValue(value: WidgetTypeInfo | string | null): void { |
|||
this.searchText = ''; |
|||
if (value != null) { |
|||
if (typeof value === 'string') { |
|||
this.widgetService.getWidgetTypeInfoById(value).subscribe( |
|||
(widgetType) => { |
|||
this.modelValue = widgetType; |
|||
this.selectWidgetTypeFormGroup.get('widgetType').patchValue(widgetType, {emitEvent: false}); |
|||
} |
|||
); |
|||
} else { |
|||
this.modelValue = value; |
|||
this.selectWidgetTypeFormGroup.get('widgetType').patchValue(value, {emitEvent: false}); |
|||
} |
|||
} else { |
|||
this.modelValue = null; |
|||
this.selectWidgetTypeFormGroup.get('widgetType').patchValue('', {emitEvent: false}); |
|||
} |
|||
this.dirty = true; |
|||
} |
|||
|
|||
updateView(value: WidgetTypeInfo | null) { |
|||
if (this.modelValue !== value) { |
|||
this.modelValue = value; |
|||
this.propagateChange(this.modelValue); |
|||
} |
|||
} |
|||
|
|||
displayWidgetTypeFn(widgetType?: WidgetTypeInfo): string | undefined { |
|||
return widgetType ? widgetType.name : undefined; |
|||
} |
|||
|
|||
getPreviewImage(imageUrl: string | null): SafeUrl | string { |
|||
if (isDefinedAndNotNull(imageUrl)) { |
|||
return this.sanitizer.bypassSecurityTrustUrl(imageUrl); |
|||
} |
|||
return '/assets/widget-preview-empty.svg'; |
|||
} |
|||
|
|||
fetchWidgetTypes(searchText?: string): Observable<Array<WidgetTypeInfo>> { |
|||
this.searchText = searchText; |
|||
const pageLink = new PageLink(10, 0, searchText, { |
|||
property: 'name', |
|||
direction: Direction.ASC |
|||
}); |
|||
return this.getWidgetTypes(pageLink); |
|||
} |
|||
|
|||
getWidgetTypes(pageLink: PageLink, |
|||
result: Array<WidgetTypeInfo> = []): Observable<Array<WidgetTypeInfo>> { |
|||
return this.widgetService.getWidgetTypes(pageLink, true).pipe( |
|||
catchError(() => of(emptyPageData<WidgetTypeInfo>())), |
|||
switchMap((data) => { |
|||
if (this.excludeWidgetTypeIds?.length) { |
|||
const filtered = data.data.filter(w => !this.excludeWidgetTypeIds.includes(w.id.id)); |
|||
result = result.concat(filtered); |
|||
} else { |
|||
result = data.data; |
|||
} |
|||
if (result.length >= pageLink.pageSize || !this.excludeWidgetTypeIds?.length || !data.hasNext) { |
|||
return of(result); |
|||
} else { |
|||
return this.getWidgetTypes(pageLink.nextPageLink(), result); |
|||
} |
|||
}) |
|||
); |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.selectWidgetTypeFormGroup.get('widgetType').updateValueAndValidity({onlySelf: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
clear() { |
|||
this.selectWidgetTypeFormGroup.get('widgetType').patchValue(''); |
|||
setTimeout(() => { |
|||
this.widgetTypeInput.nativeElement.blur(); |
|||
this.widgetTypeInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<!-- |
|||
|
|||
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-tab *ngIf="isTenantWidgetType() && authUser.authority === authorities.TENANT_ADMIN" |
|||
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab"> |
|||
<tb-version-control detailsMode="true" singleEntityMode="true" |
|||
(versionRestored)="entitiesTableConfig.updateData()" |
|||
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control> |
|||
</mat-tab> |
|||
@ -0,0 +1,43 @@ |
|||
///
|
|||
/// 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 { EntityTabsComponent } from '../../components/entity/entity-tabs.component'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
import { WidgetTypeDetails } from '@shared/models/widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-type-tabs', |
|||
templateUrl: './widget-type-tabs.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class WidgetTypeTabsComponent extends EntityTabsComponent<WidgetTypeDetails> { |
|||
|
|||
constructor(protected store: Store<AppState>) { |
|||
super(store); |
|||
} |
|||
|
|||
isTenantWidgetType() { |
|||
return this.entity && this.entity.tenantId.id !== NULL_UUID; |
|||
} |
|||
|
|||
ngOnInit() { |
|||
super.ngOnInit(); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
<!-- |
|||
|
|||
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. |
|||
|
|||
--> |
|||
<div class="tb-details-buttons" fxLayout.xs="column"> |
|||
<button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onEntityAction($event, 'edit')" |
|||
[fxShow]="!isEdit"> |
|||
{{'widget.edit-widget-type' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onEntityAction($event, 'export')" |
|||
[fxShow]="!isEdit"> |
|||
{{'widget-type.export' | translate }} |
|||
</button> |
|||
<button mat-raised-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="onEntityAction($event, 'delete')" |
|||
[fxShow]="!hideDelete() && !isEdit"> |
|||
{{'widget.delete-widget-type' | translate }} |
|||
</button> |
|||
</div> |
|||
<div class="mat-padding" fxLayout="column"> |
|||
<form [formGroup]="entityForm"> |
|||
<fieldset [disabled]="(isLoading$ | async) || !isEdit"> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>widget.title</mat-label> |
|||
<input matInput formControlName="name" required> |
|||
<mat-error *ngIf="entityForm.get('name').hasError('required')"> |
|||
{{ 'widget.title-required' | translate }} |
|||
</mat-error> |
|||
<mat-error *ngIf="entityForm.get('name').hasError('maxlength')"> |
|||
{{ 'widget.title-max-length' | translate }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
<tb-image-input fxFlex |
|||
label="{{'widget.image-preview' | translate}}" |
|||
maxSizeByte="524288" |
|||
formControlName="image"> |
|||
</tb-image-input> |
|||
<mat-form-field class="mat-block"> |
|||
<mat-label translate>widget.description</mat-label> |
|||
<textarea matInput formControlName="description" rows="2" maxlength="255" #descriptionInput></textarea> |
|||
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint> |
|||
</mat-form-field> |
|||
<mat-slide-toggle formControlName="deprecated"> |
|||
{{ 'widget.deprecated' | translate }} |
|||
</mat-slide-toggle> |
|||
</fieldset> |
|||
</form> |
|||
</div> |
|||
@ -0,0 +1,68 @@ |
|||
///
|
|||
/// 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 { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { EntityComponent } from '../../components/entity/entity.component'; |
|||
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; |
|||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
|||
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; |
|||
import { WidgetTypeDetails } from '@shared/models/widget.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-widget-type', |
|||
templateUrl: './widget-type.component.html', |
|||
styleUrls: [] |
|||
}) |
|||
export class WidgetTypeComponent extends EntityComponent<WidgetTypeDetails> { |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
@Inject('entity') protected entityValue: WidgetTypeDetails, |
|||
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetTypeDetails>, |
|||
public fb: UntypedFormBuilder, |
|||
protected cd: ChangeDetectorRef) { |
|||
super(store, fb, entityValue, entitiesTableConfigValue, cd); |
|||
} |
|||
|
|||
hideDelete() { |
|||
if (this.entitiesTableConfig) { |
|||
return !this.entitiesTableConfig.deleteEnabled(this.entity); |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
buildForm(entity: WidgetTypeDetails): UntypedFormGroup { |
|||
return this.fb.group( |
|||
{ |
|||
name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], |
|||
image: [entity ? entity.image : ''], |
|||
description: [entity ? entity.description : '', Validators.maxLength(255)], |
|||
deprecated: [entity ? entity.deprecated : false] |
|||
} |
|||
); |
|||
} |
|||
|
|||
updateForm(entity: WidgetTypeDetails) { |
|||
this.entityForm.patchValue({ |
|||
name: entity.name, |
|||
image: entity.image, |
|||
description: entity.description, |
|||
deprecated: entity.deprecated |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,232 @@ |
|||
///
|
|||
/// 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 { Injectable } from '@angular/core'; |
|||
|
|||
import { Resolve, Router } from '@angular/router'; |
|||
import { |
|||
checkBoxCell, |
|||
DateEntityTableColumn, |
|||
EntityTableColumn, |
|||
EntityTableConfig |
|||
} from '@home/models/entity/entities-table-config.models'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
import { DatePipe } from '@angular/common'; |
|||
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; |
|||
import { EntityAction } from '@home/models/entity/entity-component.models'; |
|||
import { WidgetService } from '@app/core/http/widget.service'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { DialogService } from '@core/services/dialog.service'; |
|||
import { ImportExportService } from '@home/components/import-export/import-export.service'; |
|||
import { Direction } from '@shared/models/page/sort-order'; |
|||
import { |
|||
BaseWidgetType, |
|||
WidgetTypeDetails, |
|||
WidgetTypeInfo, |
|||
widgetType as WidgetDataType, |
|||
widgetTypesData |
|||
} from '@shared/models/widget.models'; |
|||
import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component'; |
|||
import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.component'; |
|||
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; |
|||
import { MatDialog } from '@angular/material/dialog'; |
|||
|
|||
@Injectable() |
|||
export class WidgetTypesTableConfigResolver implements Resolve<EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails>> { |
|||
|
|||
private readonly config: EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails> = |
|||
new EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails>(); |
|||
|
|||
constructor(private store: Store<AppState>, |
|||
private dialog: MatDialog, |
|||
private widgetsService: WidgetService, |
|||
private translate: TranslateService, |
|||
private importExport: ImportExportService, |
|||
private datePipe: DatePipe, |
|||
private router: Router) { |
|||
|
|||
this.config.entityType = EntityType.WIDGETS_BUNDLE; |
|||
this.config.entityComponent = WidgetTypeComponent; |
|||
this.config.entityTabsComponent = WidgetTypeTabsComponent; |
|||
this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGET_TYPE); |
|||
this.config.entityResources = entityTypeResources.get(EntityType.WIDGET_TYPE); |
|||
this.config.defaultSortOrder = {property: 'name', direction: Direction.ASC}; |
|||
|
|||
this.config.rowPointer = true; |
|||
|
|||
this.config.entityTitle = (widgetType) => widgetType ? |
|||
widgetType.name : ''; |
|||
|
|||
this.config.columns.push( |
|||
new DateEntityTableColumn<WidgetTypeInfo>('createdTime', 'common.created-time', this.datePipe, '150px'), |
|||
new EntityTableColumn<WidgetTypeInfo>('name', 'widget.title', '100%'), |
|||
new EntityTableColumn<WidgetTypeInfo>('widgetType', 'widget.type', '150px', entity => |
|||
entity?.widgetType ? this.translate.instant(widgetTypesData.get(entity.widgetType).name) : '', undefined, false), |
|||
new EntityTableColumn<WidgetTypeInfo>('tenantId', 'widget.system', '60px', |
|||
entity => checkBoxCell(entity.tenantId.id === NULL_UUID)), |
|||
new EntityTableColumn<WidgetTypeInfo>('deprecated', 'widget.deprecated', '60px', |
|||
entity => checkBoxCell(entity.deprecated)) |
|||
); |
|||
|
|||
this.config.addActionDescriptors.push( |
|||
{ |
|||
name: this.translate.instant('widget-type.create-new-widget-type'), |
|||
icon: 'insert_drive_file', |
|||
isEnabled: () => true, |
|||
onAction: ($event) => this.addWidgetType($event) |
|||
}, |
|||
{ |
|||
name: this.translate.instant('widget-type.import'), |
|||
icon: 'file_upload', |
|||
isEnabled: () => true, |
|||
onAction: ($event) => this.importWidgetType($event) |
|||
} |
|||
); |
|||
|
|||
this.config.cellActionDescriptors.push( |
|||
{ |
|||
name: this.translate.instant('widget-type.export'), |
|||
icon: 'file_download', |
|||
isEnabled: () => true, |
|||
onAction: ($event, entity) => this.exportWidgetType($event, entity) |
|||
}, |
|||
{ |
|||
name: this.translate.instant('widget.widget-type-details'), |
|||
icon: 'edit', |
|||
isEnabled: () => true, |
|||
onAction: ($event, entity) => this.config.toggleEntityDetails($event, entity) |
|||
} |
|||
); |
|||
|
|||
this.config.groupActionDescriptors.push( |
|||
{ |
|||
name: this.translate.instant('widget-type.export-widget-types'), |
|||
icon: 'file_download', |
|||
isEnabled: true, |
|||
onAction: ($event, entities) => this.exportWidgetTypes($event, entities) |
|||
} |
|||
); |
|||
|
|||
this.config.deleteEntityTitle = widgetType => this.translate.instant('widget.delete-widget-type-title', |
|||
{ widgetTypeName: widgetType.name }); |
|||
this.config.deleteEntityContent = () => this.translate.instant('widget.delete-widget-type-text'); |
|||
this.config.deleteEntitiesTitle = count => this.translate.instant('widget.delete-widget-types-title', {count}); |
|||
this.config.deleteEntitiesContent = () => this.translate.instant('widget.delete-widget-types-text'); |
|||
|
|||
|
|||
this.config.loadEntity = id => this.widgetsService.getWidgetTypeById(id.id); |
|||
this.config.saveEntity = widgetType => this.widgetsService.saveWidgetType(widgetType as WidgetTypeDetails); |
|||
this.config.deleteEntity = id => this.widgetsService.deleteWidgetType(id.id); |
|||
this.config.onEntityAction = action => this.onWidgetTypeAction(action); |
|||
|
|||
this.config.handleRowClick = ($event, widgetType) => { |
|||
if (this.config.isDetailsOpen()) { |
|||
this.config.toggleEntityDetails($event, widgetType); |
|||
} else { |
|||
this.openWidgetEditor($event, widgetType); |
|||
} |
|||
return true; |
|||
}; |
|||
} |
|||
|
|||
resolve(): EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails> { |
|||
this.config.tableTitle = this.translate.instant('widget.widget-types'); |
|||
const authUser = getCurrentAuthUser(this.store); |
|||
this.config.deleteEnabled = (widgetType) => this.isWidgetTypeEditable(widgetType, authUser.authority); |
|||
this.config.entitySelectionEnabled = (widgetType) => this.isWidgetTypeEditable(widgetType, authUser.authority); |
|||
this.config.detailsReadonly = (widgetType) => !this.isWidgetTypeEditable(widgetType, authUser.authority); |
|||
const authState = getCurrentAuthState(this.store); |
|||
this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetTypes(pageLink); |
|||
return this.config; |
|||
} |
|||
|
|||
isWidgetTypeEditable(widgetType: BaseWidgetType, authority: Authority): boolean { |
|||
if (authority === Authority.TENANT_ADMIN) { |
|||
return widgetType && widgetType.tenantId && widgetType.tenantId.id !== NULL_UUID; |
|||
} else { |
|||
return authority === Authority.SYS_ADMIN; |
|||
} |
|||
} |
|||
|
|||
addWidgetType($event: Event): void { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.dialog.open<SelectWidgetTypeDialogComponent, any, |
|||
WidgetDataType>(SelectWidgetTypeDialogComponent, { |
|||
disableClose: true, |
|||
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'] |
|||
}).afterClosed().subscribe( |
|||
(type) => { |
|||
if (type) { |
|||
this.router.navigateByUrl(`resources/widgets-library/widget-types/${type}`); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
importWidgetType($event: Event) { |
|||
this.importExport.importWidgetType().subscribe( |
|||
(widgetType) => { |
|||
if (widgetType) { |
|||
this.config.updateData(); |
|||
} |
|||
} |
|||
); |
|||
} |
|||
|
|||
openWidgetEditor($event: Event, widgetType: BaseWidgetType) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`); |
|||
} |
|||
|
|||
exportWidgetType($event: Event, widgetType: BaseWidgetType) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.importExport.exportWidgetType(widgetType.id.id); |
|||
} |
|||
|
|||
exportWidgetTypes($event: Event, widgetTypes: BaseWidgetType[]) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.importExport.exportWidgetTypes(widgetTypes.map(w => w.id.id)).subscribe( |
|||
() => { |
|||
this.config.getTable().clearSelection(); |
|||
} |
|||
); |
|||
} |
|||
|
|||
onWidgetTypeAction(action: EntityAction<BaseWidgetType>): boolean { |
|||
switch (action.action) { |
|||
case 'edit': |
|||
this.openWidgetEditor(action.event, action.entity); |
|||
return true; |
|||
case 'export': |
|||
this.exportWidgetType(action.event, action.entity); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
<!-- |
|||
|
|||
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-card appearance="outlined" class="tb-bundle-widgets-card"> |
|||
<mat-card-header> |
|||
<mat-card-title> |
|||
<div class="title-container"> |
|||
<button mat-icon-button |
|||
matTooltip="{{ 'action.back' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="goBack()"> |
|||
<mat-icon>arrow_back</mat-icon> |
|||
</button> |
|||
<span class="mat-headline-5">{{ widgetsBundle.title }}: {{ 'widget.widgets' | translate }}</span> |
|||
<span fxFlex></span> |
|||
<button mat-icon-button |
|||
matTooltip="{{ 'widgets-bundle.export' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="exportWidgetsBundle()"> |
|||
<mat-icon>file_download</mat-icon> |
|||
</button> |
|||
</div> |
|||
</mat-card-title> |
|||
</mat-card-header> |
|||
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> |
|||
</mat-progress-bar> |
|||
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div> |
|||
<mat-card-content> |
|||
<div class="tb-drop-list tb-bundle-widgets-container" cdkDropList cdkDropListOrientation="vertical" |
|||
(cdkDropListDropped)="widgetDrop($event)" [cdkDropListDisabled]="!editMode || widgets?.length < 2"> |
|||
<div cdkDrag class="tb-draggable tb-bundle-widget-row" *ngFor="let widget of widgets; trackBy: trackByWidget; |
|||
let $index = index; last as isLast;"> |
|||
<div class="tb-bundle-widget-row-details"> |
|||
<img class="tb-bundle-widget-image-preview" [src]="getPreviewImage(widget.image)" alt="{{ widget.name }}"> |
|||
<div class="tb-bundle-widget-details"> |
|||
<div class="tb-bundle-widget-title">{{ widget.name }}</div> |
|||
<div *ngIf="widget.deprecated" class="tb-bundle-widget-deprecated">{{ 'widget.deprecated' | translate }}</div> |
|||
</div> |
|||
</div> |
|||
<button *ngIf="!editMode" |
|||
mat-icon-button |
|||
(click)="openWidgetEditor($event, widget)" |
|||
matTooltip="{{'widget.edit-widget-type' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>edit</mat-icon> |
|||
</button> |
|||
<button *ngIf="!editMode" |
|||
mat-icon-button |
|||
(click)="exportWidgetType($event, widget)" |
|||
matTooltip="{{'widget-type.export' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>file_download</mat-icon> |
|||
</button> |
|||
<button *ngIf="editMode && !addMode" |
|||
mat-icon-button |
|||
(click)="removeWidgetType($event, widget)" |
|||
matTooltip="{{'widget.remove' | translate }}" |
|||
matTooltipPosition="above"> |
|||
<mat-icon>delete</mat-icon> |
|||
</button> |
|||
<button cdkDragHandle *ngIf="editMode" |
|||
[fxShow]="widgets?.length > 1" |
|||
mat-icon-button |
|||
matTooltip="{{ 'action.drag' | translate }}" |
|||
matTooltipPosition="above" |
|||
class="tb-drag-handle"> |
|||
<tb-icon>drag_indicator</tb-icon> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="editMode && !addMode && widgets?.length" class="tb-add-widget-button" |
|||
matTooltip="{{ 'widget.add' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="addWidgetMode()"> |
|||
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon> |
|||
</div> |
|||
<div *ngIf="editMode && !addMode && !widgets?.length" class="tb-add-widget-button-panel"> |
|||
<div class="tb-add-widget-button-with-text" |
|||
matTooltip="{{ 'widget.add' | translate }}" |
|||
matTooltipPosition="above" |
|||
(click)="addWidgetMode()"> |
|||
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon> |
|||
<div class="tb-add-widget-button-text" translate>widget.add</div> |
|||
</div> |
|||
</div> |
|||
<div *ngIf="isReadOnly && !widgets?.length" class="tb-no-data-available"> |
|||
<div class="tb-no-data-bg"></div> |
|||
<div class="tb-no-data-text" translate>widget.no-widgets</div> |
|||
</div> |
|||
<div *ngIf="addMode" class="tb-add-bundle-widget-form"> |
|||
<tb-widget-type-autocomplete |
|||
required |
|||
placeholder="{{ 'widget.select-widget' | translate }}" |
|||
[excludeWidgetTypeIds]="excludeWidgetTypeIds" |
|||
[formControl]="addWidgetFormControl"> |
|||
</tb-widget-type-autocomplete> |
|||
<div class="tb-add-bundle-widget-actions"> |
|||
<button mat-button color="primary" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancelAddWidgetMode()">{{'action.cancel' | translate}} |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</mat-card-content> |
|||
<mat-card-actions> |
|||
<button mat-button color="primary" *ngIf="editMode && (widgets?.length || isDirty)" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="cancel()">{{'action.cancel' | translate}} |
|||
</button> |
|||
<button mat-raised-button color="primary" *ngIf="editMode && (widgets?.length || isDirty)" |
|||
[disabled]="(isLoading$ | async) || !isDirty" |
|||
(click)="save()">{{'action.save' | translate}} |
|||
</button> |
|||
<button mat-raised-button color="primary" *ngIf="!isReadOnly && !editMode" |
|||
[disabled]="(isLoading$ | async)" |
|||
(click)="edit()">{{'action.edit' | translate}} |
|||
</button> |
|||
</mat-card-actions> |
|||
</mat-card> |
|||
|
|||
@ -0,0 +1,184 @@ |
|||
/** |
|||
* 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 "../../../../../scss/constants"; |
|||
|
|||
:host { |
|||
width: 100%; |
|||
height: 100%; |
|||
padding: 8px; |
|||
} |
|||
|
|||
:host ::ng-deep { |
|||
.tb-bundle-widgets-card { |
|||
.mat-mdc-card-header-text { |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-bundle-widgets-card { |
|||
padding: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
@media #{$mat-md} { |
|||
width: 80%; |
|||
} |
|||
|
|||
@media #{$mat-gt-md} { |
|||
width: 60%; |
|||
} |
|||
|
|||
.title-container { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
gap: 8px; |
|||
.mat-headline-5 { |
|||
margin: 0; |
|||
} |
|||
} |
|||
|
|||
.mat-mdc-card-content { |
|||
flex: 1; |
|||
padding: 16px; |
|||
min-height: 0; |
|||
overflow: hidden; |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.tb-add-widget-button { |
|||
min-height: 56px; |
|||
cursor: pointer; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background: #FFFFFF; |
|||
padding: 8px; |
|||
border: 2px dashed rgba(0, 0, 0, 0.08); |
|||
border-radius: 10px; |
|||
} |
|||
|
|||
.tb-add-widget-button-panel { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
.tb-add-widget-button-with-text { |
|||
cursor: pointer; |
|||
display: flex; |
|||
padding: 24px 40px 24px 24px; |
|||
justify-content: center; |
|||
align-items: center; |
|||
gap: 8px; |
|||
border: 2px dashed rgba(0, 0, 0, 0.12); |
|||
border-radius: 10px; |
|||
.tb-add-widget-button-text { |
|||
font-size: 16px; |
|||
font-style: normal; |
|||
font-weight: 500; |
|||
line-height: 24px; |
|||
letter-spacing: 0.25px; |
|||
color: $tb-primary-color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.tb-add-bundle-widget-form { |
|||
display: flex; |
|||
padding: 16px; |
|||
flex-direction: column; |
|||
gap: 12px; |
|||
border-radius: 10px; |
|||
border: 1px solid rgba(0, 0, 0, 0.05); |
|||
box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04); |
|||
} |
|||
|
|||
.tb-add-bundle-widget-actions { |
|||
display: flex; |
|||
justify-content: flex-end; |
|||
align-items: flex-start; |
|||
gap: 16px; |
|||
} |
|||
|
|||
.mat-mdc-card-actions { |
|||
justify-content: end; |
|||
gap: 8px; |
|||
} |
|||
} |
|||
|
|||
.tb-bundle-widgets-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 8px; |
|||
min-height: 0; |
|||
overflow: auto; |
|||
padding-top: 8px; |
|||
padding-bottom: 8px; |
|||
} |
|||
|
|||
.tb-bundle-widget-row { |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
padding: 6px 12px 6px 6px; |
|||
background: #fff; |
|||
gap: 12px; |
|||
border-radius: 10px; |
|||
border: 1px solid rgba(0, 0, 0, 0.05); |
|||
box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04); |
|||
.tb-bundle-widget-row-details { |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
gap: 12px; |
|||
.tb-bundle-widget-image-preview { |
|||
width: 64px; |
|||
max-height: 100%; |
|||
object-fit: contain; |
|||
border-radius: 6px; |
|||
} |
|||
.tb-bundle-widget-details { |
|||
padding: 18px 0; |
|||
flex: 1; |
|||
display: flex; |
|||
flex-direction: row; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
.tb-bundle-widget-title { |
|||
font-size: 14px; |
|||
font-weight: 400; |
|||
letter-spacing: 0.2px; |
|||
color: rgba(0, 0, 0, 0.76); |
|||
} |
|||
.tb-bundle-widget-deprecated { |
|||
font-size: 13px; |
|||
font-weight: 400; |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
} |
|||
} |
|||
.mat-icon { |
|||
color: rgba(0, 0, 0, 0.54); |
|||
} |
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
///
|
|||
/// 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 { ChangeDetectorRef, Component, OnInit } from '@angular/core'; |
|||
import { PageComponent } from '@shared/components/page.component'; |
|||
import { AuthUser } from '@shared/models/user.model'; |
|||
import { Store } from '@ngrx/store'; |
|||
import { AppState } from '@core/core.state'; |
|||
import { ActivatedRoute, Router } from '@angular/router'; |
|||
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; |
|||
import { Authority } from '@shared/models/authority.enum'; |
|||
import { NULL_UUID } from '@shared/models/id/has-uuid'; |
|||
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; |
|||
import { WidgetTypeInfo } from '@shared/models/widget.models'; |
|||
import { CdkDragDrop } from '@angular/cdk/drag-drop'; |
|||
import { ImportExportService } from '@home/components/import-export/import-export.service'; |
|||
import { WidgetService } from '@core/http/widget.service'; |
|||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; |
|||
import { isDefinedAndNotNull } from '@core/utils'; |
|||
import { FormControl, Validators } from '@angular/forms'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-widgets-bundle-widget', |
|||
templateUrl: './widgets-bundle-widgets.component.html', |
|||
styleUrls: ['./widgets-bundle-widgets.component.scss'] |
|||
}) |
|||
export class WidgetsBundleWidgetsComponent extends PageComponent implements OnInit { |
|||
|
|||
authUser: AuthUser; |
|||
|
|||
isReadOnly: boolean; |
|||
editMode = false; |
|||
addMode = false; |
|||
isDirty = false; |
|||
|
|||
widgetsBundle: WidgetsBundle; |
|||
widgets: Array<WidgetTypeInfo>; |
|||
excludeWidgetTypeIds: Array<string>; |
|||
|
|||
addWidgetFormControl = new FormControl(null, [Validators.required]); |
|||
|
|||
constructor(protected store: Store<AppState>, |
|||
private router: Router, |
|||
private route: ActivatedRoute, |
|||
private widgetsService: WidgetService, |
|||
private importExport: ImportExportService, |
|||
private sanitizer: DomSanitizer, |
|||
private cd: ChangeDetectorRef) { |
|||
super(store); |
|||
this.authUser = getCurrentAuthUser(this.store); |
|||
this.widgetsBundle = this.route.snapshot.data.widgetsBundle; |
|||
this.widgets = [...this.route.snapshot.data.widgets]; |
|||
if (this.authUser.authority === Authority.TENANT_ADMIN) { |
|||
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; |
|||
} else { |
|||
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; |
|||
} |
|||
if (!this.isReadOnly && !this.widgets.length) { |
|||
this.editMode = true; |
|||
} |
|||
this.addWidgetFormControl.valueChanges.subscribe((newWidget) => { |
|||
if (newWidget) { |
|||
this.addWidget(newWidget); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
} |
|||
|
|||
getPreviewImage(imageUrl: string | null): SafeUrl | string { |
|||
if (isDefinedAndNotNull(imageUrl)) { |
|||
return this.sanitizer.bypassSecurityTrustUrl(imageUrl); |
|||
} |
|||
return '/assets/widget-preview-empty.svg'; |
|||
} |
|||
|
|||
trackByWidget(index: number, widget: WidgetTypeInfo): any { |
|||
return widget; |
|||
} |
|||
|
|||
widgetDrop(event: CdkDragDrop<string[]>) { |
|||
const widget = this.widgets[event.previousIndex]; |
|||
this.widgets.splice(event.previousIndex, 1); |
|||
this.widgets.splice(event.currentIndex, 0, widget); |
|||
this.isDirty = true; |
|||
} |
|||
|
|||
addWidgetMode() { |
|||
this.addWidgetFormControl.patchValue(null, {emitEvent: false}); |
|||
this.excludeWidgetTypeIds = this.widgets.map(w => w.id.id); |
|||
this.addMode = true; |
|||
} |
|||
|
|||
cancelAddWidgetMode() { |
|||
this.addMode = false; |
|||
} |
|||
|
|||
private addWidget(newWidget: WidgetTypeInfo) { |
|||
this.widgets.push(newWidget); |
|||
this.isDirty = true; |
|||
this.addMode = false; |
|||
} |
|||
|
|||
openWidgetEditor($event: Event, widgetType: WidgetTypeInfo) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`); |
|||
} |
|||
|
|||
exportWidgetType($event: Event, widgetType: WidgetTypeInfo) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
this.importExport.exportWidgetType(widgetType.id.id); |
|||
} |
|||
|
|||
removeWidgetType($event: Event, widgetType: WidgetTypeInfo) { |
|||
if ($event) { |
|||
$event.stopPropagation(); |
|||
} |
|||
const index = this.widgets.indexOf(widgetType); |
|||
this.widgets.splice(index, 1); |
|||
this.isDirty = true; |
|||
} |
|||
|
|||
goBack() { |
|||
this.router.navigate(['..'], { relativeTo: this.route }); |
|||
} |
|||
|
|||
exportWidgetsBundle() { |
|||
this.importExport.exportWidgetsBundle(this.widgetsBundle.id.id); |
|||
} |
|||
|
|||
edit() { |
|||
this.editMode = true; |
|||
} |
|||
|
|||
cancel() { |
|||
if (this.isDirty) { |
|||
this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).subscribe( |
|||
(widgets) => { |
|||
this.widgets = [...widgets]; |
|||
this.isDirty = false; |
|||
this.addMode = false; |
|||
this.editMode = !this.widgets.length; |
|||
this.cd.markForCheck(); |
|||
} |
|||
); |
|||
} else { |
|||
this.addMode = false; |
|||
this.editMode = !this.widgets.length; |
|||
} |
|||
} |
|||
|
|||
save() { |
|||
const widgetTypeIds = this.widgets.map(w => w.id.id); |
|||
this.widgetsService.updateWidgetsBundleWidgetTypes(this.widgetsBundle.id.id, widgetTypeIds).subscribe(() => { |
|||
this.isDirty = false; |
|||
this.editMode = false; |
|||
}); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue