Browse Source

Merge pull request #11365 from maxunbearable/feature/entities-version-ui-implementation

Version Conflict UI implementation
pull/11112/head
Andrew Shvayka 2 years ago
committed by GitHub
parent
commit
687dbf4c16
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      ui-ngx/src/app/core/core.module.ts
  2. 90
      ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts
  3. 46
      ui-ngx/src/app/core/interceptors/global-http-interceptor.ts
  4. 1
      ui-ngx/src/app/core/interceptors/interceptor-config.ts
  5. 46
      ui-ngx/src/app/core/interceptors/interceptor.util.ts
  6. 53
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html
  7. 26
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.scss
  8. 61
      ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts
  9. 34
      ui-ngx/src/app/shared/import-export/import-export.service.ts
  10. 6
      ui-ngx/src/app/shared/models/asset.models.ts
  11. 4
      ui-ngx/src/app/shared/models/customer.model.ts
  12. 4
      ui-ngx/src/app/shared/models/dashboard.models.ts
  13. 8
      ui-ngx/src/app/shared/models/device.models.ts
  14. 4
      ui-ngx/src/app/shared/models/edge.models.ts
  15. 4
      ui-ngx/src/app/shared/models/entity-view.models.ts
  16. 4
      ui-ngx/src/app/shared/models/entity.models.ts
  17. 4
      ui-ngx/src/app/shared/models/rule-chain.models.ts
  18. 4
      ui-ngx/src/app/shared/models/widgets-bundle.model.ts
  19. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

6
ui-ngx/src/app/core/core.module.ts

@ -41,6 +41,7 @@ import { WINDOW_PROVIDERS } from '@core/services/window.service';
import { HotkeyModule } from 'angular2-hotkeys';
import { TranslateDefaultParser } from '@core/translate/translate-default-parser';
import { TranslateDefaultLoader } from '@core/translate/translate-default-loader';
import { EntityConflictInterceptor } from '@core/interceptors/entity-conflict.interceptor';
@NgModule({
imports: [
@ -95,6 +96,11 @@ import { TranslateDefaultLoader } from '@core/translate/translate-default-loader
useClass: GlobalHttpInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: EntityConflictInterceptor,
multi: true
},
{
provide: MAT_DIALOG_DEFAULT_OPTIONS,
useValue: {

90
ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts

@ -0,0 +1,90 @@
///
/// Copyright © 2016-2024 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 {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpStatusCode
} from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators';
import { MatDialog } from '@angular/material/dialog';
import {
EntityConflictDialogComponent
} from '@shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component';
import { HasId } from '@shared/models/base-data';
import { HasVersion } from '@shared/models/entity.models';
import { getInterceptorConfig } from './interceptor.util';
@Injectable()
export class EntityConflictInterceptor implements HttpInterceptor {
constructor(
private dialog: MatDialog,
) {}
intercept(request: HttpRequest<unknown & HasId & HasVersion>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (!request.url.startsWith('/api/')) {
return next.handle(request);
}
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status !== HttpStatusCode.Conflict) {
return throwError(() => error);
}
return this.handleConflictError(request, next, error);
})
);
}
private handleConflictError(
request: HttpRequest<unknown & HasId & HasVersion>,
next: HttpHandler,
error: HttpErrorResponse
): Observable<HttpEvent<unknown>> {
if (getInterceptorConfig(request).ignoreVersionConflict) {
return throwError(() => error);
}
return this.openConflictDialog(request.body, error.error.message).pipe(
switchMap(result => {
if (result) {
return next.handle(this.updateRequestVersion(request));
}
return of(null);
})
);
}
private updateRequestVersion(request: HttpRequest<unknown & HasId & HasVersion>): HttpRequest<unknown & HasId & HasVersion> {
const body = { ...request.body, version: null };
return request.clone({ body });
}
private openConflictDialog(entity: unknown & HasId & HasVersion, message: string): Observable<boolean> {
const dialogRef = this.dialog.open(EntityConflictDialogComponent, {
data: { message, entity }
});
return dialogRef.afterClosed();
}
}

46
ui-ngx/src/app/core/interceptors/global-http-interceptor.ts

@ -16,10 +16,9 @@
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs/internal/Observable';
import { Inject, Injectable } from '@angular/core';
import { Injectable } from '@angular/core';
import { AuthService } from '@core/auth/auth.service';
import { Constants } from '@shared/models/constants';
import { InterceptorHttpParams } from './interceptor-http-params';
import { catchError, delay, finalize, mergeMap, switchMap } from 'rxjs/operators';
import { of, throwError } from 'rxjs';
import { InterceptorConfig } from './interceptor-config';
@ -30,6 +29,7 @@ import { ActionNotificationShow } from '@app/core/notification/notification.acti
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { parseHttpErrorMessage } from '@core/utils';
import { getInterceptorConfig } from './interceptor.util';
const tmpHeaders = {};
@ -39,22 +39,18 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
private AUTH_SCHEME = 'Bearer ';
private AUTH_HEADER_NAME = 'X-Authorization';
private internalUrlPrefixes = [
'/api/auth/token',
'/api/rpc'
];
private activeRequests = 0;
constructor(@Inject(Store) private store: Store<AppState>,
@Inject(DialogService) private dialogService: DialogService,
@Inject(TranslateService) private translate: TranslateService,
@Inject(AuthService) private authService: AuthService) {
}
constructor(
private store: Store<AppState>,
private dialogService: DialogService,
private translate: TranslateService,
private authService: AuthService,
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.startsWith('/api/')) {
const config = this.getInterceptorConfig(req);
const config = getInterceptorConfig(req);
this.updateLoadingState(config, true);
let observable$: Observable<HttpEvent<any>>;
if (this.isTokenBasedAuthEntryPoint(req.url)) {
@ -98,7 +94,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
private handleResponseError(req: HttpRequest<any>, next: HttpHandler, errorResponse: HttpErrorResponse): Observable<HttpEvent<any>> {
const config = this.getInterceptorConfig(req);
const config = getInterceptorConfig(req);
let unhandled = false;
const ignoreErrors = config.ignoreErrors;
const resendRequest = config.resendRequest;
@ -171,15 +167,6 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
}
private isInternalUrlPrefix(url: string): boolean {
for (const index in this.internalUrlPrefixes) {
if (url.startsWith(this.internalUrlPrefixes[index])) {
return true;
}
}
return false;
}
private isTokenBasedAuthEntryPoint(url: string): boolean {
return url.startsWith('/api/') &&
!url.startsWith(Constants.entryPoints.login) &&
@ -202,19 +189,6 @@ export class GlobalHttpInterceptor implements HttpInterceptor {
}
}
private getInterceptorConfig(req: HttpRequest<any>): InterceptorConfig {
let config: InterceptorConfig;
if (req.params && req.params instanceof InterceptorHttpParams) {
config = (req.params as InterceptorHttpParams).interceptorConfig;
} else {
config = new InterceptorConfig(false, false);
}
if (this.isInternalUrlPrefix(req.url)) {
config.ignoreLoading = true;
}
return config;
}
private showError(error: string, timeout: number = 0) {
setTimeout(() => {
this.store.dispatch(new ActionNotificationShow({message: error, type: 'error'}));

1
ui-ngx/src/app/core/interceptors/interceptor-config.ts

@ -17,5 +17,6 @@
export class InterceptorConfig {
constructor(public ignoreLoading: boolean = false,
public ignoreErrors: boolean = false,
public ignoreVersionConflict: boolean = false,
public resendRequest: boolean = false) {}
}

46
ui-ngx/src/app/core/interceptors/interceptor.util.ts

@ -0,0 +1,46 @@
///
/// Copyright © 2016-2024 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 { HttpRequest } from '@angular/common/http';
import { InterceptorConfig } from '@core/interceptors/interceptor-config';
import { InterceptorHttpParams } from '@core/interceptors/interceptor-http-params';
const internalUrlPrefixes = [
'/api/auth/token',
'/api/rpc'
];
export const getInterceptorConfig = (req: HttpRequest<unknown>): InterceptorConfig => {
let config: InterceptorConfig;
if (req.params && req.params instanceof InterceptorHttpParams) {
config = (req.params as InterceptorHttpParams).interceptorConfig;
} else {
config = new InterceptorConfig();
}
if (isInternalUrlPrefix(req.url)) {
config.ignoreLoading = true;
}
return config;
};
const isInternalUrlPrefix = (url: string): boolean => {
for (const prefix of internalUrlPrefixes) {
if (url.startsWith(prefix)) {
return true;
}
}
return false;
};

53
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.html

@ -0,0 +1,53 @@
<!--
Copyright © 2016-2024 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 class="main-label">{{ 'entity.version-conflict.label' | translate }}</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="onCancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
<div class="message-container">
<span>{{ data.message }}.</span>
<span>
{{ 'entity.version-conflict.link' | translate:
{ entityType: (entityTypeTranslations.get(data.entity.id.entityType).type | translate) }
}}
<a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>.
</span>
<span>{{ 'entity.version-conflict.message' | translate }}</span>
</div>
</div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
(click)="onCancel()"
cdkFocusInitial
>
{{ 'entity.version-conflict.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
(click)="onConfirm()"
>
{{ 'entity.version-conflict.overwrite' | translate }}
</button>
</div>

26
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.scss

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2024 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.
*/
$conflict-dialog-width: 700px;
:host {
.main-label {
padding-left: 8px;
}
.message-container {
max-width: #{$conflict-dialog-width};
}
}

61
ui-ngx/src/app/shared/components/dialog/entity-conflict-dialog/entity-conflict-dialog.component.ts

@ -0,0 +1,61 @@
///
/// Copyright © 2016-2024 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 } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { SharedModule } from '@shared/shared.module';
import { ImportExportService } from '@shared/import-export/import-export.service';
import { CommonModule } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityInfoData } from '@shared/models/entity.models';
interface EntityConflictDialogData {
message: string;
entity: EntityInfoData;
}
@Component({
selector: 'tb-entity-conflict-dialog',
templateUrl: 'entity-conflict-dialog.component.html',
styleUrls: ['./entity-conflict-dialog.component.scss'],
standalone: true,
imports: [
CommonModule,
SharedModule,
],
})
export class EntityConflictDialogComponent {
readonly entityTypeTranslations = entityTypeTranslations;
constructor(
@Inject(MAT_DIALOG_DATA) public data: EntityConflictDialogData,
private dialogRef: MatDialogRef<EntityConflictDialogComponent>,
private importExportService: ImportExportService,
) {}
onCancel(): void {
this.dialogRef.close(false);
}
onConfirm(): void {
this.dialogRef.close(true);
}
onLinkClick(event: MouseEvent): void {
event.preventDefault();
this.importExportService.exportEntity(this.data.entity);
}
}

34
ui-ngx/src/app/shared/import-export/import-export.service.ts

@ -55,7 +55,7 @@ import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
import { EntityInfoData, ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
import { RequestConfig } from '@core/http/http-utils';
import { RuleChain, RuleChainImport, RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
@ -380,6 +380,35 @@ export class ImportExportService {
});
}
public exportEntity(entityData: EntityInfoData): void {
let preparedData;
switch (entityData.id.entityType) {
case EntityType.DEVICE_PROFILE:
case EntityType.ASSET_PROFILE:
preparedData = this.prepareProfileExport(entityData as DeviceProfile | AssetProfile);
break;
case EntityType.RULE_CHAIN:
this.ruleChainService.getRuleChainMetadata(entityData.id.id)
.pipe(
take(1),
map((ruleChainMetaData) => {
const ruleChainExport: RuleChainImport = {
ruleChain: this.prepareRuleChain(entityData as RuleChain),
metadata: this.prepareRuleChainMetaData(ruleChainMetaData)
};
return ruleChainExport;
}))
.subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name));
return;
case EntityType.DASHBOARD:
preparedData = this.prepareDashboardExport(entityData as Dashboard);
break;
default:
preparedData = this.prepareExport(entityData);
}
this.exportToPc(preparedData, entityData.name);
}
private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) {
this.widgetService.exportBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe({
next: (widgetTypesDetails) => {
@ -1108,6 +1137,9 @@ export class ImportExportService {
if (isDefined(exportedData.externalId)) {
delete exportedData.externalId;
}
if (isDefined(exportedData.version)) {
delete exportedData.version;
}
return exportedData;
}

6
ui-ngx/src/app/shared/models/asset.models.ts

@ -22,9 +22,9 @@ import { EntitySearchQuery } from '@shared/models/relation.models';
import { AssetProfileId } from '@shared/models/id/asset-profile-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { DashboardId } from '@shared/models/id/dashboard-id';
import { EntityInfoData, HasTenantId } from '@shared/models/entity.models';
import { EntityInfoData, HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface AssetProfile extends BaseData<AssetProfileId>, HasTenantId, ExportableEntity<AssetProfileId> {
export interface AssetProfile extends BaseData<AssetProfileId>, HasTenantId, HasVersion, ExportableEntity<AssetProfileId> {
tenantId?: TenantId;
name: string;
description?: string;
@ -42,7 +42,7 @@ export interface AssetProfileInfo extends EntityInfoData {
defaultDashboardId?: DashboardId;
}
export interface Asset extends BaseData<AssetId>, HasTenantId, ExportableEntity<AssetId> {
export interface Asset extends BaseData<AssetId>, HasTenantId, HasVersion, ExportableEntity<AssetId> {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;

4
ui-ngx/src/app/shared/models/customer.model.ts

@ -18,9 +18,9 @@ import { CustomerId } from '@shared/models/id/customer-id';
import { ContactBased } from '@shared/models/contact-based.model';
import { TenantId } from './id/tenant-id';
import { ExportableEntity } from '@shared/models/base-data';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface Customer extends ContactBased<CustomerId>, HasTenantId, ExportableEntity<CustomerId> {
export interface Customer extends ContactBased<CustomerId>, HasTenantId, HasVersion, ExportableEntity<CustomerId> {
tenantId: TenantId;
title: string;
additionalInfo?: any;

4
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -23,9 +23,9 @@ import { Timewindow } from '@shared/models/time/time.models';
import { EntityAliases } from './alias.models';
import { Filters } from '@shared/models/query/query.models';
import { MatDialogRef } from '@angular/material/dialog';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface DashboardInfo extends BaseData<DashboardId>, HasTenantId, ExportableEntity<DashboardId> {
export interface DashboardInfo extends BaseData<DashboardId>, HasTenantId, HasVersion, ExportableEntity<DashboardId> {
tenantId?: TenantId;
title?: string;
image?: string;

8
ui-ngx/src/app/shared/models/device.models.ts

@ -22,7 +22,7 @@ import { DeviceCredentialsId } from '@shared/models/id/device-credentials-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { EntityInfoData, HasTenantId } from '@shared/models/entity.models';
import { EntityInfoData, HasTenantId, HasVersion } from '@shared/models/entity.models';
import { FilterPredicateValue, KeyFilter } from '@shared/models/query/query.models';
import { TimeUnit } from '@shared/models/time/time.models';
import * as _moment from 'moment';
@ -584,7 +584,7 @@ export interface DeviceProfileData {
provisionConfiguration?: DeviceProvisionConfiguration;
}
export interface DeviceProfile extends BaseData<DeviceProfileId>, HasTenantId, ExportableEntity<DeviceProfileId> {
export interface DeviceProfile extends BaseData<DeviceProfileId>, HasTenantId, HasVersion, ExportableEntity<DeviceProfileId> {
tenantId?: TenantId;
name: string;
description?: string;
@ -711,7 +711,7 @@ export interface DeviceData {
transportConfiguration: DeviceTransportConfiguration;
}
export interface Device extends BaseData<DeviceId>, HasTenantId, ExportableEntity<DeviceId> {
export interface Device extends BaseData<DeviceId>, HasTenantId, HasVersion, ExportableEntity<DeviceId> {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;
@ -801,7 +801,7 @@ export const credentialTypesByTransportType = new Map<DeviceTransportType, Devic
]
);
export interface DeviceCredentials extends BaseData<DeviceCredentialsId> {
export interface DeviceCredentials extends BaseData<DeviceCredentialsId>, HasTenantId {
deviceId: DeviceId;
credentialsType: DeviceCredentialsType;
credentialsId: string;

4
ui-ngx/src/app/shared/models/edge.models.ts

@ -22,9 +22,9 @@ import { EntitySearchQuery } from '@shared/models/relation.models';
import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { BaseEventBody } from '@shared/models/event.models';
import { EventId } from '@shared/models/id/event-id';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface Edge extends BaseData<EdgeId>, HasTenantId {
export interface Edge extends BaseData<EdgeId>, HasTenantId, HasVersion {
tenantId?: TenantId;
customerId?: CustomerId;
name: string;

4
ui-ngx/src/app/shared/models/entity-view.models.ts

@ -20,7 +20,7 @@ import { CustomerId } from '@shared/models/id/customer-id';
import { EntityViewId } from '@shared/models/id/entity-view-id';
import { EntityId } from '@shared/models/id/entity-id';
import { EntitySearchQuery } from '@shared/models/relation.models';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface AttributesEntityView {
cs: Array<string>;
@ -33,7 +33,7 @@ export interface TelemetryEntityView {
attributes: AttributesEntityView;
}
export interface EntityView extends BaseData<EntityViewId>, HasTenantId, ExportableEntity<EntityViewId> {
export interface EntityView extends BaseData<EntityViewId>, HasTenantId, HasVersion, ExportableEntity<EntityViewId> {
tenantId: TenantId;
customerId: CustomerId;
entityId: EntityId;

4
ui-ngx/src/app/shared/models/entity.models.ts

@ -187,3 +187,7 @@ export const entityFields: {[fieldName: string]: EntityField} = {
export interface HasTenantId {
tenantId?: TenantId;
}
export interface HasVersion {
version?: number;
}

4
ui-ngx/src/app/shared/models/rule-chain.models.ts

@ -20,9 +20,9 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
import { RuleNodeId } from '@shared/models/id/rule-node-id';
import { RuleNode, RuleNodeComponentDescriptor, RuleNodeType } from '@shared/models/rule-node.models';
import { ComponentClusteringMode, ComponentType } from '@shared/models/component-descriptor.models';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface RuleChain extends BaseData<RuleChainId>, HasTenantId, ExportableEntity<RuleChainId> {
export interface RuleChain extends BaseData<RuleChainId>, HasTenantId, HasVersion, ExportableEntity<RuleChainId> {
tenantId: TenantId;
name: string;
firstRuleNodeId: RuleNodeId;

4
ui-ngx/src/app/shared/models/widgets-bundle.model.ts

@ -17,9 +17,9 @@
import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetsBundleId } from '@shared/models/id/widgets-bundle-id';
import { HasTenantId } from '@shared/models/entity.models';
import { HasTenantId, HasVersion } from '@shared/models/entity.models';
export interface WidgetsBundle extends BaseData<WidgetsBundleId>, HasTenantId, ExportableEntity<WidgetsBundleId> {
export interface WidgetsBundle extends BaseData<WidgetsBundleId>, HasTenantId, HasVersion, ExportableEntity<WidgetsBundleId> {
tenantId: TenantId;
alias: string;
title: string;

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

@ -2284,6 +2284,13 @@
"type-edges": "Edges",
"list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }",
"edge-name-starts-with": "Edges whose names start with '{{prefix}}'",
"version-conflict": {
"label": "Version conflict",
"message": "Do you want to cancel your changes or overwrite existing version?",
"link": "You can download your version of the {{entityType}} using this",
"overwrite": "Overwrite version",
"cancel": "Cancel changes"
},
"type-tb-resource": "Resource",
"type-tb-resources": "Resources",
"list-of-tb-resources": "{ count, plural, =1 {One resource} other {List of # resources} }",
@ -2302,7 +2309,8 @@
"type-notification-request": "Notification request",
"type-notification-template": "Notification template",
"type-notification-templates": "Notification templates",
"list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }"
"list-of-notification-templates": "{ count, plural, =1 {One notification template} other {List of # notification templates} }",
"link": "link"
},
"entity-field": {
"created-time": "Created time",

Loading…
Cancel
Save