Browse Source

Implement ability to move widget types between widget bundles. Ability to deprecate widget type.

pull/9157/head
Igor Kulikov 3 years ago
parent
commit
acdf5ad48c
  1. 43
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  2. 67
      application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java
  3. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  4. 5
      dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java
  5. 21
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  6. 26
      ui-ngx/src/app/core/http/widget.service.ts
  7. 11
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  8. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  9. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  10. 26
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  11. 56
      ui-ngx/src/app/modules/home/pages/widget/move-widget-type-dialog.component.html
  12. 82
      ui-ngx/src/app/modules/home/pages/widget/move-widget-type-dialog.component.ts
  13. 24
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  14. 113
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  15. 72
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  16. 2
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  17. 2
      ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts
  18. 9
      ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts
  19. 1
      ui-ngx/src/app/shared/models/widget.models.ts
  20. 7
      ui-ngx/src/assets/locale/locale.constant-en_US.json

43
application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java

@ -80,7 +80,7 @@ public class WidgetTypeController extends AutoCommitController {
"The newly created Widget Type Id will be present in the response. " + "The newly created Widget Type Id will be present in the response. " +
"Specify existing Widget Type id to update the Widget Type. " + "Specify existing Widget Type id to update the Widget Type. " +
"Referencing non-existing Widget Type Id will cause 'Not Found' error." + "Referencing non-existing Widget Type Id will cause 'Not Found' error." +
"\n\nWidget Type alias is unique in the scope of Widget Bundle. " + "\n\nWidget Type fqn is unique in the scope of System or Tenant. " +
"Special Tenant Id '13814000-1dd2-11b2-8080-808080808080' is automatically used if the create request is sent by user with 'SYS_ADMIN' authority." + "Special Tenant Id '13814000-1dd2-11b2-8080-808080808080' is automatically used if the create request is sent by user with 'SYS_ADMIN' authority." +
"Remove 'id', 'tenantId' rom the request body example (below) to create new Widget Type entity." + "Remove 'id', 'tenantId' rom the request body example (below) to create new Widget Type entity." +
SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ -243,8 +243,8 @@ public class WidgetTypeController extends AutoCommitController {
notes = "Set Widget Type deprecated flag. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Set Widget Type deprecated flag. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType/{widgetTypeId}/deprecate/{deprecated}", method = RequestMethod.POST) @RequestMapping(value = "/widgetType/{widgetTypeId}/deprecate/{deprecated}", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK) @ResponseBody
public void setWidgetTypeDeprecated( public WidgetTypeDetails setWidgetTypeDeprecated(
@ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true) @ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetTypeId") String strWidgetTypeId, @PathVariable("widgetTypeId") String strWidgetTypeId,
@PathVariable("deprecated") boolean deprecated) throws Exception { @PathVariable("deprecated") boolean deprecated) throws Exception {
@ -252,14 +252,45 @@ public class WidgetTypeController extends AutoCommitController {
var currentUser = getCurrentUser(); var currentUser = getCurrentUser();
WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId)); WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId));
WidgetTypeDetails wtd = checkWidgetTypeId(widgetTypeId, Operation.WRITE); WidgetTypeDetails wtd = checkWidgetTypeId(widgetTypeId, Operation.WRITE);
widgetTypeService.setWidgetTypeDeprecated(currentUser.getTenantId(), widgetTypeId, deprecated); WidgetTypeDetails updated = widgetTypeService.setWidgetTypeDeprecated(currentUser.getTenantId(), widgetTypeId, deprecated);
if (!Authority.SYS_ADMIN.equals(currentUser.getAuthority())) {
if (wtd != null && !Authority.SYS_ADMIN.equals(currentUser.getAuthority())) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(wtd.getTenantId(), wtd.getBundleAlias()); WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(wtd.getTenantId(), wtd.getBundleAlias());
if (widgetsBundle != null) { if (widgetsBundle != null) {
autoCommit(currentUser, widgetsBundle.getId()); autoCommit(currentUser, widgetsBundle.getId());
} }
} }
return updated;
}
@ApiOperation(value = "Move widget type to target widgets bundle (moveWidgetType)",
notes = "Move Widget Type to target Widgets Bundle. Referencing non-existing Widget Type Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType/{widgetTypeId}/move", params = {"targetBundleAlias"}, method = RequestMethod.POST)
@ResponseBody
public WidgetTypeDetails moveWidgetType(
@ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetTypeId") String strWidgetTypeId,
@ApiParam(value = "Target Widget Bundle alias", required = true)
@RequestParam String targetBundleAlias) throws Exception {
checkParameter("widgetTypeId", strWidgetTypeId);
checkParameter("targetBundleAlias", targetBundleAlias);
var currentUser = getCurrentUser();
WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId));
WidgetTypeDetails wtd = checkWidgetTypeId(widgetTypeId, Operation.WRITE);
if (!wtd.getBundleAlias().equals(targetBundleAlias)) {
wtd = widgetTypeService.moveWidgetType(currentUser.getTenantId(), widgetTypeId, targetBundleAlias);
if (!Authority.SYS_ADMIN.equals(currentUser.getAuthority())) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(wtd.getTenantId(), wtd.getBundleAlias());
if (widgetsBundle != null) {
autoCommit(currentUser, widgetsBundle.getId());
}
WidgetsBundle targetWidgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(currentUser.getTenantId(), targetBundleAlias);
if (targetWidgetsBundle != null) {
autoCommit(currentUser, targetWidgetsBundle.getId());
}
}
}
return wtd;
} }
} }

67
application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java

@ -191,10 +191,30 @@ public class WidgetTypeControllerTest extends AbstractControllerTest {
widgetType.setName("Widget Type"); widgetType.setName("Widget Type");
widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class)); widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class); WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
WidgetsBundle widgetsBundle2 = new WidgetsBundle();
widgetsBundle2.setTitle("My widgets bundle 2");
WidgetsBundle savedWidgetsBundle2 = doPost("/api/widgetsBundle", widgetsBundle2, WidgetsBundle.class);
savedWidgetType.setBundleAlias(savedWidgetsBundle2.getAlias());
doPost("/api/widgetType", savedWidgetType);
WidgetTypeDetails foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetTypeDetails.class);
Assert.assertEquals(savedWidgetsBundle2.getAlias(), foundWidgetType.getBundleAlias());
}
@Test
public void testUpdateWidgetTypeBundleAliasToNonExistent() throws Exception {
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
savedWidgetType.setBundleAlias("some_alias"); savedWidgetType.setBundleAlias("some_alias");
doPost("/api/widgetType", savedWidgetType) doPost("/api/widgetType", savedWidgetType)
.andExpect(status().isBadRequest()) .andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Update of widget type bundle alias is prohibited"))); .andExpect(statusReason(containsString("Widget type is referencing to non-existent widgets bundle")));
} }
@ -212,6 +232,51 @@ public class WidgetTypeControllerTest extends AbstractControllerTest {
} }
@Test
public void testDeprecateWidgetType() throws Exception {
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
doPost("/api/widgetType/"+savedWidgetType.getId().getId().toString() + "/deprecate/true")
.andExpect(status().isOk());
WidgetTypeDetails foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetTypeDetails.class);
Assert.assertTrue(foundWidgetType.isDeprecated());
doPost("/api/widgetType/"+savedWidgetType.getId().getId().toString() + "/deprecate/false")
.andExpect(status().isOk());
foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetTypeDetails.class);
Assert.assertFalse(foundWidgetType.isDeprecated());
}
@Test
public void testMoveWidgetType() throws Exception {
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
WidgetsBundle widgetsBundle2 = new WidgetsBundle();
widgetsBundle2.setTitle("My widgets bundle 2");
WidgetsBundle savedWidgetsBundle2 = doPost("/api/widgetsBundle", widgetsBundle2, WidgetsBundle.class);
doPost("/api/widgetType/"+savedWidgetType.getId().getId().toString() + "/move?targetBundleAlias=" + savedWidgetsBundle2.getAlias())
.andExpect(status().isOk());
WidgetTypeDetails foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetTypeDetails.class);
Assert.assertEquals(savedWidgetsBundle2.getAlias(), foundWidgetType.getBundleAlias());
}
@Test
public void testMoveWidgetTypeToNonExistentBundle() throws Exception {
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(JacksonUtil.fromString("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
doPost("/api/widgetType/"+savedWidgetType.getId().getId().toString() + "/move?targetBundleAlias=some_alias")
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Widget type is referencing to non-existent widgets bundle")));
}
@Test @Test
public void testGetBundleWidgetTypes() throws Exception { public void testGetBundleWidgetTypes() throws Exception {
List<WidgetType> widgetTypes = new ArrayList<>(); List<WidgetType> widgetTypes = new ArrayList<>();

4
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java

@ -33,9 +33,11 @@ public interface WidgetTypeService extends EntityDaoService {
WidgetTypeDetails saveWidgetType(WidgetTypeDetails widgetType); WidgetTypeDetails saveWidgetType(WidgetTypeDetails widgetType);
WidgetTypeDetails moveWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId, String targetBundleAlias);
void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId); void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId);
void setWidgetTypeDeprecated(TenantId tenantId, WidgetTypeId widgetTypeId, boolean deprecated); WidgetTypeDetails setWidgetTypeDeprecated(TenantId tenantId, WidgetTypeId widgetTypeId, boolean deprecated);
List<WidgetType> findWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias); List<WidgetType> findWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);

5
dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java

@ -87,7 +87,10 @@ public class WidgetTypeDataValidator extends DataValidator<WidgetTypeDetails> {
throw new DataValidationException("Can't move existing widget type to different tenant!"); throw new DataValidationException("Can't move existing widget type to different tenant!");
} }
if (!storedWidgetType.getBundleAlias().equals(widgetTypeDetails.getBundleAlias())) { if (!storedWidgetType.getBundleAlias().equals(widgetTypeDetails.getBundleAlias())) {
throw new DataValidationException("Update of widget type bundle alias is prohibited!"); WidgetsBundle widgetsBundle = widgetsBundleDao.findWidgetsBundleByTenantIdAndAlias(widgetTypeDetails.getTenantId().getId(), widgetTypeDetails.getBundleAlias());
if (widgetsBundle == null) {
throw new DataValidationException("Widget type is referencing to non-existent widgets bundle!");
}
} }
if (!storedWidgetType.getFqn().equals(widgetTypeDetails.getFqn())) { if (!storedWidgetType.getFqn().equals(widgetTypeDetails.getFqn())) {
throw new DataValidationException("Update of widget type fqn is prohibited!"); throw new DataValidationException("Update of widget type fqn is prohibited!");

21
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -84,6 +84,21 @@ public class WidgetTypeServiceImpl extends AbstractEntityService implements Widg
} }
} }
@Override
public WidgetTypeDetails moveWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId, String targetBundleAlias) {
log.trace("Executing moveWidgetType, widgetTypeId [{}], targetBundleAlias [{}]", widgetTypeId, targetBundleAlias);
Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
WidgetTypeDetails widgetTypeDetails = widgetTypeDao.findById(tenantId, widgetTypeId.getId());
if (widgetTypeDetails != null && !widgetTypeDetails.getBundleAlias().equals(targetBundleAlias)) {
widgetTypeDetails.setBundleAlias(targetBundleAlias);
widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId);
widgetTypeDetails = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails);
eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(widgetTypeDetails.getTenantId())
.entityId(widgetTypeDetails.getId()).added(widgetTypeDetails.getId() == null).build());
}
return widgetTypeDetails;
}
@Override @Override
public void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId) { public void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId) {
log.trace("Executing deleteWidgetType [{}]", widgetTypeId); log.trace("Executing deleteWidgetType [{}]", widgetTypeId);
@ -93,14 +108,15 @@ public class WidgetTypeServiceImpl extends AbstractEntityService implements Widg
} }
@Override @Override
public void setWidgetTypeDeprecated(TenantId tenantId, WidgetTypeId widgetTypeId, boolean deprecated) { public WidgetTypeDetails setWidgetTypeDeprecated(TenantId tenantId, WidgetTypeId widgetTypeId, boolean deprecated) {
log.trace("Executing setWidgetTypeDeprecated, widgetTypeId [{}], deprecated [{}]", widgetTypeId, deprecated); log.trace("Executing setWidgetTypeDeprecated, widgetTypeId [{}], deprecated [{}]", widgetTypeId, deprecated);
Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId); Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
WidgetTypeDetails widgetTypeDetails = widgetTypeDao.findById(tenantId, widgetTypeId.getId()); WidgetTypeDetails widgetTypeDetails = widgetTypeDao.findById(tenantId, widgetTypeId.getId());
if (widgetTypeDetails.isDeprecated() != deprecated) { if (widgetTypeDetails.isDeprecated() != deprecated) {
widgetTypeDetails.setDeprecated(deprecated); widgetTypeDetails.setDeprecated(deprecated);
widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails); widgetTypeDetails = widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails);
} }
return widgetTypeDetails;
} }
@Override @Override
@ -117,6 +133,7 @@ public class WidgetTypeServiceImpl extends AbstractEntityService implements Widg
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias); Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias);
return widgetTypeDao.findWidgetTypesDetailsByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias); return widgetTypeDao.findWidgetTypesDetailsByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
} }
@Override @Override

26
ui-ngx/src/app/core/http/widget.service.ts

@ -152,9 +152,12 @@ export class WidgetService {
return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe( return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe(
map((types) => { map((types) => {
types = types.sort((a, b) => { types = types.sort((a, b) => {
let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); let result = (a.deprecated ? 1 : 0) - (b.deprecated ? 1 : 0);
if (result === 0) { if (result === 0) {
result = b.createdTime - a.createdTime; result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);
if (result === 0) {
result = b.createdTime - a.createdTime;
}
} }
return result; return result;
}); });
@ -180,6 +183,9 @@ export class WidgetService {
}; };
widget.config.title = widgetTypeInfo.widgetName; widget.config.title = widgetTypeInfo.widgetName;
if (type.deprecated) {
widget.config.title += ` (${this.translate.instant('widget.deprecated')})`;
}
widgetTypes.push(widget); widgetTypes.push(widget);
top += sizeY; top += sizeY;
@ -216,6 +222,22 @@ export class WidgetService {
})); }));
} }
public setWidgetTypeDeprecated(widgetTypeId: string, deprecated: boolean, config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}/deprecate/${deprecated}`,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {
this.widgetTypeUpdated(savedWidgetType);
}));
}
public moveWidgetType(widgetTypeId: string, targetBundleAlias: string, config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}/move?targetBundleAlias=${targetBundleAlias}`,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {
this.widgetTypeUpdated(savedWidgetType);
}));
}
public saveImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails, public saveImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails,
config?: RequestConfig): Observable<WidgetTypeDetails> { config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails, return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails,

11
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -408,7 +408,7 @@
(ngModelChange)="searchBundle = $event"> (ngModelChange)="searchBundle = $event">
</tb-widgets-bundle-search> </tb-widgets-bundle-search>
</div> </div>
<div class="details-buttons" *ngIf="isAddingWidget"> <div class="details-buttons" *ngIf="isAddingWidget" fxLayout="row" fxLayoutAlign="start center">
<button mat-button type="button" (click)="importWidget($event)" <button mat-button type="button" (click)="importWidget($event)"
*ngIf="!dashboardWidgetSelectComponent?.widgetsBundle"> *ngIf="!dashboardWidgetSelectComponent?.widgetsBundle">
<mat-icon>file_upload</mat-icon>{{ 'dashboard.import-widget' | translate }}</button> <mat-icon>file_upload</mat-icon>{{ 'dashboard.import-widget' | translate }}</button>
@ -419,6 +419,15 @@
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>filter_list</mat-icon> <mat-icon>filter_list</mat-icon>
</button> </button>
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.hasDeprecated"
appearance="fill-invert"
disablePagination
selectMediaBreakpoint="xs"
[(ngModel)]="dashboardWidgetSelectComponent.widgetsListMode">
<tb-toggle-option value="all">{{ 'widget.all' | translate }}</tb-toggle-option>
<tb-toggle-option value="actual">{{ 'widget.actual' | translate }}</tb-toggle-option>
<tb-toggle-option value="deprecated">{{ 'widget.deprecated' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div> </div>
<tb-dashboard-widget-select #dashboardWidgetSelect <tb-dashboard-widget-select #dashboardWidgetSelect
*ngIf="isAddingWidget" *ngIf="isAddingWidget"

2
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html

@ -24,7 +24,7 @@
<img class="preview" [src]="getPreviewImage(widget.image)" alt="{{ widget.title }}"> <img class="preview" [src]="getPreviewImage(widget.image)" alt="{{ widget.title }}">
</div> </div>
<div fxFlex fxLayout="column"> <div fxFlex fxLayout="column">
<mat-card-title>{{widget.title}}</mat-card-title> <mat-card-title>{{widget.title}}<div *ngIf="widget.deprecated" class="tb-deprecated" translate>widget.deprecated</div></mat-card-title>
<mat-card-subtitle>{{ 'widget.' + widget.type | translate }}</mat-card-subtitle> <mat-card-subtitle>{{ 'widget.' + widget.type | translate }}</mat-card-subtitle>
<mat-card-content *ngIf="widget.description"> <mat-card-content *ngIf="widget.description">
{{ widget.description }} {{ widget.description }}

4
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss

@ -54,6 +54,10 @@
font-size: 20px; font-size: 20px;
line-height: normal; line-height: normal;
margin-bottom: 8px; margin-bottom: 8px;
.tb-deprecated {
font-size: 14px;
color: rgba(209, 39, 48, 0.87);
}
} }
.mat-mdc-card-subtitle { .mat-mdc-card-subtitle {

26
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -25,6 +25,8 @@ import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
type widgetsListMode = 'all' | 'actual' | 'deprecated';
@Component({ @Component({
selector: 'tb-dashboard-widget-select', selector: 'tb-dashboard-widget-select',
templateUrl: './dashboard-widget-select.component.html', templateUrl: './dashboard-widget-select.component.html',
@ -34,9 +36,11 @@ export class DashboardWidgetSelectComponent implements OnInit {
private search$ = new BehaviorSubject<string>(''); private search$ = new BehaviorSubject<string>('');
private filterWidgetTypes$ = new BehaviorSubject<Array<widgetType>>(null); private filterWidgetTypes$ = new BehaviorSubject<Array<widgetType>>(null);
private widgetsListMode$ = new BehaviorSubject<widgetsListMode>('actual');
private widgetsInfo: Observable<Array<WidgetInfo>>; private widgetsInfo: Observable<Array<WidgetInfo>>;
private widgetsBundleValue: WidgetsBundle; private widgetsBundleValue: WidgetsBundle;
widgetTypes = new Set<widgetType>(); widgetTypes = new Set<widgetType>();
hasDeprecated = false;
widgets$: Observable<Array<WidgetInfo>>; widgets$: Observable<Array<WidgetInfo>>;
loadingWidgetsSubject: BehaviorSubject<boolean> = new BehaviorSubject(false); loadingWidgetsSubject: BehaviorSubject<boolean> = new BehaviorSubject(false);
@ -54,8 +58,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
this.widgetsBundleValue = widgetBundle; this.widgetsBundleValue = widgetBundle;
if (widgetBundle === null) { if (widgetBundle === null) {
this.widgetTypes.clear(); this.widgetTypes.clear();
this.hasDeprecated = false;
} }
this.filterWidgetTypes$.next(null); this.filterWidgetTypes$.next(null);
this.widgetsListMode$.next('actual');
this.widgetsInfo = null; this.widgetsInfo = null;
} }
} }
@ -81,6 +87,15 @@ export class DashboardWidgetSelectComponent implements OnInit {
return this.filterWidgetTypes$.value; return this.filterWidgetTypes$.value;
} }
@Input()
set widgetsListMode(mode: widgetsListMode) {
this.widgetsListMode$.next(mode);
}
get widgetsListMode(): widgetsListMode {
return this.widgetsListMode$.value;
}
@Output() @Output()
widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>(); widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();
@ -94,7 +109,7 @@ export class DashboardWidgetSelectComponent implements OnInit {
distinctUntilChanged(), distinctUntilChanged(),
switchMap(search => this.fetchWidgetBundle(search)) switchMap(search => this.fetchWidgetBundle(search))
); );
this.widgets$ = combineLatest([this.search$.asObservable(), this.filterWidgetTypes$.asObservable()]).pipe( this.widgets$ = combineLatest([this.search$.asObservable(), this.filterWidgetTypes$.asObservable(), this.widgetsListMode$]).pipe(
distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)), distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)),
switchMap(search => this.fetchWidget(...search)) switchMap(search => this.fetchWidget(...search))
); );
@ -113,6 +128,7 @@ export class DashboardWidgetSelectComponent implements OnInit {
map(widgets => { map(widgets => {
widgets = widgets.sort((a, b) => b.createdTime - a.createdTime); widgets = widgets.sort((a, b) => b.createdTime - a.createdTime);
const widgetTypes = new Set<widgetType>(); const widgetTypes = new Set<widgetType>();
const hasDeprecated = widgets.some(w => w.deprecated);
const widgetInfos = widgets.map((widgetTypeInfo) => { const widgetInfos = widgets.map((widgetTypeInfo) => {
widgetTypes.add(widgetTypeInfo.widgetType); widgetTypes.add(widgetTypeInfo.widgetType);
const widget: WidgetInfo = { const widget: WidgetInfo = {
@ -120,13 +136,15 @@ export class DashboardWidgetSelectComponent implements OnInit {
type: widgetTypeInfo.widgetType, type: widgetTypeInfo.widgetType,
title: widgetTypeInfo.name, title: widgetTypeInfo.name,
image: widgetTypeInfo.image, image: widgetTypeInfo.image,
description: widgetTypeInfo.description description: widgetTypeInfo.description,
deprecated: widgetTypeInfo.deprecated
}; };
return widget; return widget;
} }
); );
setTimeout(() => { setTimeout(() => {
this.widgetTypes = widgetTypes; this.widgetTypes = widgetTypes;
this.hasDeprecated = hasDeprecated;
this.cd.markForCheck(); this.cd.markForCheck();
}); });
return widgetInfos; return widgetInfos;
@ -185,8 +203,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
); );
} }
private fetchWidget(search: string, filter: widgetType[]): Observable<Array<WidgetInfo>> { private fetchWidget(search: string, filter: widgetType[], listMode: widgetsListMode): Observable<Array<WidgetInfo>> {
return this.getWidgets().pipe( return this.getWidgets().pipe(
map(widgets => (listMode && listMode !== 'all') ?
widgets.filter((widget) => listMode === 'actual' ? !widget.deprecated : widget.deprecated) : widgets),
map(widgets => filter ? widgets.filter((widget) => filter.includes(widget.type)) : widgets), map(widgets => filter ? widgets.filter((widget) => filter.includes(widget.type)) : widgets),
map(widgets => search ? widgets.filter( map(widgets => search ? widgets.filter(
widget => ( widget => (

56
ui-ngx/src/app/modules/home/pages/widget/move-widget-type-dialog.component.html

@ -0,0 +1,56 @@
<!--
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>

82
ui-ngx/src/app/modules/home/pages/widget/move-widget-type-dialog.component.ts

@ -0,0 +1,82 @@
///
/// 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);
}
}

24
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -63,9 +63,18 @@
[tb-circular-progress]="saveWidgetAsPending" [tb-circular-progress]="saveWidgetAsPending"
matTooltip="{{ 'widget.saveAs' | translate }} (Shift + CTRL + S)" matTooltip="{{ 'widget.saveAs' | translate }} (Shift + CTRL + S)"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>save</mat-icon> <mat-icon>save_as</mat-icon>
<span translate>action.saveAs</span> <span translate>action.saveAs</span>
</button> </button>
<button mat-raised-button
fxHide.lt-md [disabled]="(isLoading$ | async) || moveDisabled()"
(click)="moveWidget()"
[tb-circular-progress]="moveWidgetPending"
matTooltip="{{ 'widget.move' | translate }} (Shift + CTRL + M)"
matTooltipPosition="below">
<tb-icon matButtonIcon>mdi:content-save-move</tb-icon>
<span translate>action.move</span>
</button>
<button mat-button <button mat-button
fxHide.lt-lg fxHide.lt-lg
(click)="fullscreen = !fullscreen" (click)="fullscreen = !fullscreen"
@ -106,9 +115,15 @@
<button mat-menu-item <button mat-menu-item
[disabled]="(isLoading$ | async) || saveAsDisabled()" [disabled]="(isLoading$ | async) || saveAsDisabled()"
(click)="saveWidgetAs()"> (click)="saveWidgetAs()">
<mat-icon>save</mat-icon> <mat-icon>save_as</mat-icon>
<span translate>action.saveAs</span> <span translate>action.saveAs</span>
</button> </button>
<button mat-menu-item
[disabled]="(isLoading$ | async) || moveDisabled()"
(click)="moveWidget()">
<tb-icon matMenuItemIcon>mdi:content-save-move</tb-icon>
<span translate>action.move</span>
</button>
</mat-menu> </mat-menu>
</mat-toolbar> </mat-toolbar>
<div fxFlex style="position: relative;"> <div fxFlex style="position: relative;">
@ -253,6 +268,11 @@
rows="2" maxlength="255"></textarea> rows="2" maxlength="255"></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint> <mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint>
</mat-form-field> </mat-form-field>
<mat-slide-toggle class="mat-block" style="padding-bottom: 16px;"
[(ngModel)]="widget.deprecated"
(ngModelChange)="isDirty = true">
{{ 'widget.deprecated' | translate }}
</mat-slide-toggle>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>widget.settings-form-selector</mat-label> <mat-label translate>widget.settings-form-selector</mat-label>
<input matInput <input matInput

113
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts

@ -15,20 +15,22 @@
/// ///
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import {
Component,
ElementRef,
EventEmitter,
Inject,
OnDestroy,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { detailsToWidgetInfo, toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; import { detailsToWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { import { Widget, WidgetConfig, widgetType, WidgetTypeDetails, widgetTypesData } from '@shared/models/widget.models';
Widget,
WidgetConfig,
WidgetType,
widgetType,
WidgetTypeDetails,
widgetTypesData
} from '@shared/models/widget.models';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard'; import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
@ -51,13 +53,18 @@ import {
SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogComponent,
SaveWidgetTypeAsDialogResult SaveWidgetTypeAsDialogResult
} from '@home/pages/widget/save-widget-type-as-dialog.component'; } from '@home/pages/widget/save-widget-type-as-dialog.component';
import { forkJoin, from, Subscription } from 'rxjs'; import { forkJoin, Subscription } from 'rxjs';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import Timeout = NodeJS.Timeout;
import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models'; import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models'; import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models';
import {
MoveWidgetTypeDialogComponent,
MoveWidgetTypeDialogData,
MoveWidgetTypeDialogResult
} from '@home/pages/widget/move-widget-type-dialog.component';
import Timeout = NodeJS.Timeout;
// @dynamic // @dynamic
@Component({ @Component({
@ -148,6 +155,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
iframeWidgetEditModeInited = false; iframeWidgetEditModeInited = false;
saveWidgetPending = false; saveWidgetPending = false;
saveWidgetAsPending = false; saveWidgetAsPending = false;
moveWidgetPending = false;
gotError = false; gotError = false;
errorMarkers: number[] = []; errorMarkers: number[] = [];
@ -157,6 +165,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
hotKeys: Hotkey[] = []; hotKeys: Hotkey[] = [];
updateBreadcrumbs = new EventEmitter();
private rxSubscriptions = new Array<Subscription>(); private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -249,6 +259,16 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}, ['INPUT', 'SELECT', 'TEXTAREA'], }, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.saveAs')) this.translate.instant('widget.saveAs'))
); );
this.hotKeys.push(
new Hotkey('shift+ctrl+m', (event: KeyboardEvent) => {
if (!getCurrentIsLoading(this.store) && !this.moveDisabled()) {
event.preventDefault();
this.moveWidget();
}
return false;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.move'))
);
this.hotKeys.push( this.hotKeys.push(
new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => { new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
@ -542,17 +562,22 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private commitSaveWidget() { private commitSaveWidget() {
const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined; const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined;
const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined; const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined;
this.widgetService.saveWidgetTypeDetails(this.widget, id, this.widgetsBundle.alias, createdTime).subscribe( this.widgetService.saveWidgetTypeDetails(this.widget, id, this.widgetsBundle.alias, createdTime).subscribe({
(widgetTypeDetails) => { next: (widgetTypeDetails) => {
this.setWidgetTypeDetails(widgetTypeDetails);
this.saveWidgetPending = false; this.saveWidgetPending = false;
if (!this.widgetTypeDetails?.id) {
this.isDirty = false;
this.router.navigate(['..', widgetTypeDetails.id.id], {relativeTo: this.route});
} else {
this.setWidgetTypeDetails(widgetTypeDetails);
}
this.store.dispatch(new ActionNotificationShow( this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500})); {message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500}));
}, },
() => { error: () => {
this.saveWidgetPending = false; this.saveWidgetPending = false;
} }
); });
} }
private commitSaveWidgetAs() { private commitSaveWidgetAs() {
@ -570,12 +595,18 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.widget.defaultConfig = JSON.stringify(config); this.widget.defaultConfig = JSON.stringify(config);
this.isDirty = false; this.isDirty = false;
this.widgetService.saveWidgetTypeDetails(this.widget, undefined, saveWidgetAsData.bundleAlias, undefined).subscribe( this.widgetService.saveWidgetTypeDetails(this.widget, undefined, saveWidgetAsData.bundleAlias, undefined).subscribe(
(widgetTypeDetails) => { {
this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeDetails.id.id}`); next: (widgetTypeDetails) => {
} this.saveWidgetAsPending = false;
); this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeDetails.id.id}`);
},
error: () => {
this.saveWidgetAsPending = false;
}
});
} else {
this.saveWidgetAsPending = false;
} }
this.saveWidgetAsPending = false;
} }
); );
} }
@ -587,6 +618,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.widget.defaultConfig = JSON.stringify(config); this.widget.defaultConfig = JSON.stringify(config);
this.origWidget = deepClone(this.widget); this.origWidget = deepClone(this.widget);
this.isDirty = false; this.isDirty = false;
this.updateBreadcrumbs.emit();
} }
applyWidgetScript(): void { applyWidgetScript(): void {
@ -623,6 +655,34 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.applyWidgetScript(); this.applyWidgetScript();
} }
moveWidget() {
this.moveWidgetPending = true;
this.dialog.open<MoveWidgetTypeDialogComponent, MoveWidgetTypeDialogData,
MoveWidgetTypeDialogResult>(MoveWidgetTypeDialogComponent, {
disableClose: true,
data: {
currentBundleId: this.widgetsBundle.id.id
},
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(moveWidgetTypeData) => {
if (moveWidgetTypeData) {
this.widgetService.moveWidgetType(this.widgetTypeDetails.id.id, moveWidgetTypeData.bundleAlias).subscribe({
next: (widgetTypeDetails) => {
this.moveWidgetPending = false;
this.router.navigateByUrl(`/widgets-bundles/${moveWidgetTypeData.bundleId}/widgetTypes/${widgetTypeDetails.id.id}`);
},
error: () => {
this.moveWidgetPending = false;
}
});
} else {
this.moveWidgetPending = false;
}
}
);
}
undoDisabled(): boolean { undoDisabled(): boolean {
return !this.isDirty return !this.isDirty
|| !this.iframeWidgetEditModeInited || !this.iframeWidgetEditModeInited
@ -644,6 +704,15 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
|| this.saveWidgetAsPending; || this.saveWidgetAsPending;
} }
moveDisabled(): boolean {
return this.isReadOnly
|| !this.widgetTypeDetails?.id
|| !this.iframeWidgetEditModeInited
|| this.saveWidgetPending
|| this.saveWidgetAsPending
|| this.moveWidgetPending;
}
beautifyCss(): void { beautifyCss(): void {
beautifyCss(this.widget.templateCss, {indent_size: 4}).subscribe( beautifyCss(this.widget.templateCss, {indent_size: 4}).subscribe(
(res) => { (res) => {

72
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -78,35 +78,28 @@ export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> {
resolve(route: ActivatedRouteSnapshot): Observable<WidgetEditorData> { resolve(route: ActivatedRouteSnapshot): Observable<WidgetEditorData> {
const widgetTypeId = route.params.widgetTypeId; const widgetTypeId = route.params.widgetTypeId;
return this.widgetsService.getWidgetTypeById(widgetTypeId).pipe( if (!widgetTypeId || Object.keys(widgetType).includes(widgetTypeId)) {
map((result) => ({ let widgetTypeParam = widgetTypeId as widgetType;
if (!widgetTypeParam) {
widgetTypeParam = widgetType.timeseries;
}
return this.widgetsService.getWidgetTemplate(widgetTypeParam).pipe(
map((widget) => {
widget.widgetName = null;
return {
widgetTypeDetails: null,
widget
};
})
);
} else {
return this.widgetsService.getWidgetTypeById(widgetTypeId).pipe(
map((result) => ({
widgetTypeDetails: result, widgetTypeDetails: result,
widget: detailsToWidgetInfo(result) widget: detailsToWidgetInfo(result)
})) }))
); );
}
}
@Injectable()
export class WidgetEditorAddDataResolver implements Resolve<WidgetEditorData> {
constructor(private widgetsService: WidgetService) {
}
resolve(route: ActivatedRouteSnapshot): Observable<WidgetEditorData> {
let widgetTypeParam = route.params.widgetType as widgetType;
if (!widgetTypeParam) {
widgetTypeParam = widgetType.timeseries;
} }
return this.widgetsService.getWidgetTemplate(widgetTypeParam).pipe(
map((widget) => {
widget.widgetName = null;
return {
widgetTypeDetails: null,
widget
};
})
);
} }
} }
@ -114,7 +107,9 @@ export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction<any> = (
route.data.widgetsBundle.title); route.data.widgetsBundle.title);
export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetEditorComponent> = export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetEditorComponent> =
((route, translate, component) => component ? component.widget.widgetName : ''); ((route, translate, component) =>
component?.widget?.widgetName ?
(component.widget.widgetName + (component.widget.deprecated ? ` (${translate.instant('widget.deprecated')})` : '')) : '');
export const widgetsBundlesRoutes: Routes = [ export const widgetsBundlesRoutes: Routes = [
{ {
@ -175,22 +170,6 @@ export const widgetsBundlesRoutes: Routes = [
resolve: { resolve: {
widgetEditorData: WidgetEditorDataResolver widgetEditorData: WidgetEditorDataResolver
} }
},
{
path: 'add/:widgetType',
component: WidgetEditorComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.editor',
breadcrumb: {
labelFunction: widgetEditorBreadcumbLabelFunction,
icon: 'insert_chart'
} as BreadCrumbConfig<WidgetEditorComponent>
},
resolve: {
widgetEditorData: WidgetEditorAddDataResolver
}
} }
] ]
} }
@ -216,7 +195,11 @@ const routes: Routes = [
}, },
{ {
path: 'widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType', path: 'widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType', redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetType',
},
{
path: 'resources/widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetType',
} }
]; ];
@ -228,8 +211,7 @@ const routes: Routes = [
WidgetsBundlesTableConfigResolver, WidgetsBundlesTableConfigResolver,
WidgetsBundleResolver, WidgetsBundleResolver,
WidgetsTypesDataResolver, WidgetsTypesDataResolver,
WidgetEditorDataResolver, WidgetEditorDataResolver
WidgetEditorAddDataResolver
] ]
}) })
export class WidgetLibraryRoutingModule { } export class WidgetLibraryRoutingModule { }

2
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts

@ -160,7 +160,7 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
}).afterClosed().subscribe( }).afterClosed().subscribe(
(type) => { (type) => {
if (type) { if (type) {
this.router.navigate(['add', type], {relativeTo: this.route}); this.router.navigate([type], {relativeTo: this.route});
} }
} }
); );

2
ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts

@ -25,6 +25,7 @@ import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.componen
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component'; import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component';
import { WidgetsBundleTabsComponent } from '@home/pages/widget/widgets-bundle-tabs.component'; import { WidgetsBundleTabsComponent } from '@home/pages/widget/widgets-bundle-tabs.component';
import { MoveWidgetTypeDialogComponent } from '@home/pages/widget/move-widget-type-dialog.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -33,6 +34,7 @@ import { WidgetsBundleTabsComponent } from '@home/pages/widget/widgets-bundle-ta
WidgetEditorComponent, WidgetEditorComponent,
SelectWidgetTypeDialogComponent, SelectWidgetTypeDialogComponent,
SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogComponent,
MoveWidgetTypeDialogComponent,
WidgetsBundleTabsComponent WidgetsBundleTabsComponent
], ],
imports: [ imports: [

9
ui-ngx/src/app/shared/components/widgets-bundle-select.component.ts

@ -61,6 +61,9 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni
@Input() @Input()
disabled: boolean; disabled: boolean;
@Input()
excludeBundleIds: Array<string>;
widgetsBundles$: Observable<Array<WidgetsBundle>>; widgetsBundles$: Observable<Array<WidgetsBundle>>;
widgetsBundles: Array<WidgetsBundle>; widgetsBundles: Array<WidgetsBundle>;
@ -161,6 +164,12 @@ export class WidgetsBundleSelectComponent implements ControlValueAccessor, OnIni
} else { } else {
widgetsBundlesObservable = this.widgetService.getAllWidgetsBundles(); widgetsBundlesObservable = this.widgetService.getAllWidgetsBundles();
} }
if (this.excludeBundleIds && this.excludeBundleIds.length) {
widgetsBundlesObservable = widgetsBundlesObservable.pipe(
map((widgetBundles) =>
widgetBundles.filter(w => !this.excludeBundleIds.includes(w.id.id)))
);
}
return widgetsBundlesObservable; return widgetsBundlesObservable;
} }

1
ui-ngx/src/app/shared/models/widget.models.ts

@ -698,6 +698,7 @@ export interface WidgetInfo {
title: string; title: string;
image?: string; image?: string;
description?: string; description?: string;
deprecated?: boolean;
} }
export interface GroupInfo { export interface GroupInfo {

7
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -19,6 +19,7 @@
"suspend": "Suspend", "suspend": "Suspend",
"save": "Save", "save": "Save",
"saveAs": "Save as", "saveAs": "Save as",
"move": "Move",
"cancel": "Cancel", "cancel": "Cancel",
"ok": "OK", "ok": "OK",
"delete": "Delete", "delete": "Delete",
@ -4548,8 +4549,11 @@
"unable-to-save-widget-error": "Unable to save widget! Widget has errors!", "unable-to-save-widget-error": "Unable to save widget! Widget has errors!",
"save": "Save widget", "save": "Save widget",
"saveAs": "Save widget as", "saveAs": "Save widget as",
"move": "Move widget",
"save-widget-type-as": "Save widget type as", "save-widget-type-as": "Save widget type as",
"save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle", "save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle",
"move-widget-type": "Move widget type",
"move-widget-type-text": "Please select target widgets bundle",
"toggle-fullscreen": "Toggle fullscreen", "toggle-fullscreen": "Toggle fullscreen",
"run": "Run widget", "run": "Run widget",
"title": "Widget title", "title": "Widget title",
@ -4572,6 +4576,9 @@
"settings-form-selector": "Settings form selector", "settings-form-selector": "Settings form selector",
"data-key-settings-form-selector": "Data key settings form selector", "data-key-settings-form-selector": "Data key settings form selector",
"latest-data-key-settings-form-selector": "Latest data key settings form selector", "latest-data-key-settings-form-selector": "Latest data key settings form selector",
"all": "All",
"actual": "Actual",
"deprecated": "Deprecated",
"has-basic-mode": "Has basic mode", "has-basic-mode": "Has basic mode",
"basic-mode-form-selector": "Basic mode form selector", "basic-mode-form-selector": "Basic mode form selector",
"basic-mode": "Basic", "basic-mode": "Basic",

Loading…
Cancel
Save