Merge pull request #11426 from maxunbearable/fix/version-conflict

Version Conflict dialog fixes
This commit is contained in:
Igor Kulikov 2024-08-19 18:39:20 +03:00 committed by GitHub
commit 9627a59550
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 143 additions and 59 deletions

View File

@ -185,8 +185,9 @@ export class WidgetService {
public saveWidgetTypeDetails(widgetInfo: WidgetInfo, public saveWidgetTypeDetails(widgetInfo: WidgetInfo,
id: WidgetTypeId, id: WidgetTypeId,
createdTime: number, createdTime: number,
version: number,
config?: RequestConfig): Observable<WidgetTypeDetails> { config?: RequestConfig): Observable<WidgetTypeDetails> {
const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, createdTime); const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, createdTime, version);
return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails, return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => { tap((savedWidgetType) => {

View File

@ -20,6 +20,7 @@ import {
HttpEvent, HttpEvent,
HttpHandler, HttpHandler,
HttpInterceptor, HttpInterceptor,
HttpParams,
HttpRequest, HttpRequest,
HttpStatusCode HttpStatusCode
} from '@angular/common/http'; } from '@angular/common/http';
@ -32,6 +33,8 @@ import {
import { HasId } from '@shared/models/base-data'; import { HasId } from '@shared/models/base-data';
import { HasVersion } from '@shared/models/entity.models'; import { HasVersion } from '@shared/models/entity.models';
import { getInterceptorConfig } from './interceptor.util'; import { getInterceptorConfig } from './interceptor.util';
import { isDefined } from '@core/utils';
import { InterceptorConfig } from '@core/interceptors/interceptor-config';
@Injectable() @Injectable()
export class EntityConflictInterceptor implements HttpInterceptor { export class EntityConflictInterceptor implements HttpInterceptor {
@ -67,9 +70,13 @@ export class EntityConflictInterceptor implements HttpInterceptor {
return this.openConflictDialog(request.body, error.error.message).pipe( return this.openConflictDialog(request.body, error.error.message).pipe(
switchMap(result => { switchMap(result => {
if (isDefined(result)) {
if (result) { if (result) {
return next.handle(this.updateRequestVersion(request)); return next.handle(this.updateRequestVersion(request));
} }
(request.params as HttpParams & { interceptorConfig: InterceptorConfig }).interceptorConfig.ignoreErrors = true;
return throwError(() => error);
}
return of(null); return of(null);
}) })
); );
@ -82,7 +89,9 @@ export class EntityConflictInterceptor implements HttpInterceptor {
private openConflictDialog(entity: unknown & HasId & HasVersion, message: string): Observable<boolean> { private openConflictDialog(entity: unknown & HasId & HasVersion, message: string): Observable<boolean> {
const dialogRef = this.dialog.open(EntityConflictDialogComponent, { const dialogRef = this.dialog.open(EntityConflictDialogComponent, {
data: { message, entity } disableClose: true,
data: { message, entity },
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
}); });
return dialogRef.afterClosed(); return dialogRef.afterClosed();

View File

@ -86,7 +86,7 @@ import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller'; import { AliasController } from '@core/api/alias-controller';
import { BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, of, Subject, Subscription, throwError } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import { import {
@ -147,7 +147,7 @@ import { IAliasController } from '@core/api/widget-api.models';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { distinctUntilChanged, map, skip, tap } from 'rxjs/operators'; import { catchError, distinctUntilChanged, map, skip, tap } from 'rxjs/operators';
import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models'; import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
@ -156,6 +156,7 @@ import {
MoveWidgetsDialogComponent, MoveWidgetsDialogComponent,
MoveWidgetsDialogResult MoveWidgetsDialogResult
} from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; } from '@home/components/dashboard-page/layout/move-widgets-dialog.component';
import { HttpStatusCode } from '@angular/common/http';
// @dynamic // @dynamic
@Component({ @Component({
@ -1092,7 +1093,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
public saveDashboard() { public saveDashboard() {
this.translatedDashboardTitle = this.getTranslatedDashboardTitle(); this.translatedDashboardTitle = this.getTranslatedDashboardTitle();
this.setEditMode(false, false);
this.notifyDashboardUpdated(); this.notifyDashboardUpdated();
} }
@ -1204,8 +1204,33 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: widget data: widget
}; };
this.window.parent.postMessage(JSON.stringify(message), '*'); this.window.parent.postMessage(JSON.stringify(message), '*');
this.setEditMode(false, false);
} else { } else {
this.dashboardService.saveDashboard(this.dashboard).subscribe(); let reInitDashboard = false;
this.dashboardService.saveDashboard(this.dashboard).pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {
reInitDashboard = true;
return this.dashboardService.getDashboard(this.dashboard.id.id).pipe(
map(dashboard => this.dashboardUtils.validateAndUpdateDashboard(dashboard))
);
}
return throwError(() => err);
})
).subscribe((dashboard) => {
if (reInitDashboard) {
const dashboardPageInitData: DashboardPageInitData = {
dashboard,
currentDashboardId: dashboard.id ? dashboard.id.id : null,
widgetEditMode: this.widgetEditMode,
singlePageMode: this.singlePageMode
};
this.init(dashboardPageInitData);
} else {
this.dashboard = dashboard;
this.setEditMode(false, false);
}
});
} }
} }

View File

@ -402,7 +402,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
this.cd.detectChanges(); this.cd.detectChanges();
} }
updateData(closeDetails: boolean = true) { updateData(closeDetails: boolean = true, reloadEntity: boolean = true) {
if (closeDetails) { if (closeDetails) {
this.isDetailsOpen = false; this.isDetailsOpen = false;
} }
@ -427,7 +427,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
timePageLink.endTime = interval.endTime; timePageLink.endTime = interval.endTime;
} }
this.dataSource.loadEntities(this.pageLink); this.dataSource.loadEntities(this.pageLink);
if (this.isDetailsOpen && this.entityDetailsPanel) { if (reloadEntity && this.isDetailsOpen && this.entityDetailsPanel) {
this.entityDetailsPanel.reloadEntity(); this.entityDetailsPanel.reloadEntity();
} }
} }
@ -511,7 +511,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
onEntityUpdated(entity: BaseData<HasId>) { onEntityUpdated(entity: BaseData<HasId>) {
this.updateData(false); this.updateData(false, false);
this.entitiesTableConfig.entityUpdated(entity); this.entitiesTableConfig.entityUpdated(entity);
} }

View File

@ -40,11 +40,12 @@ import { UntypedFormGroup } from '@angular/forms';
import { EntityComponent } from './entity.component'; import { EntityComponent } from './entity.component';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { EntityAction } from '@home/models/entity/entity-component.models';
import { Observable, ReplaySubject, Subscription } from 'rxjs'; import { Observable, ReplaySubject, Subscription, throwError } from 'rxjs';
import { MatTab, MatTabGroup } from '@angular/material/tabs'; import { MatTab, MatTabGroup } from '@angular/material/tabs';
import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component';
import { deepClone, mergeDeep } from '@core/utils'; import { deepClone, mergeDeep } from '@core/utils';
import { entityIdEquals } from '@shared/models/id/entity-id'; import { catchError } from 'rxjs/operators';
import { HttpStatusCode } from '@angular/common/http';
@Component({ @Component({
selector: 'tb-entity-details-panel', selector: 'tb-entity-details-panel',
@ -288,7 +289,16 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
editingEntity.additionalInfo = editingEntity.additionalInfo =
mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo); mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo);
} }
this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe( this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity)
.pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {
return this.entitiesTableConfig.loadEntity(this.currentEntityId);
}
return throwError(() => err);
})
)
.subscribe(
(entity) => { (entity) => {
this.entity = entity; this.entity = entity;
this.entityComponent.entity = entity; this.entityComponent.entity = entity;

View File

@ -113,7 +113,7 @@ export class WidgetComponentService {
hasBasicMode: this.utils.editWidgetInfo.hasBasicMode, hasBasicMode: this.utils.editWidgetInfo.hasBasicMode,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective, basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined, undefined
); );
} }
const initSubject = new ReplaySubject<void>(); const initSubject = new ReplaySubject<void>();

View File

@ -669,7 +669,7 @@ export const detailsToWidgetInfo = (widgetTypeDetailsEntity: WidgetTypeDetails):
}; };
export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
createdTime: number): WidgetType => { createdTime: number, version: number): WidgetType => {
const descriptor: WidgetTypeDescriptor = { const descriptor: WidgetTypeDescriptor = {
type: widgetInfo.type, type: widgetInfo.type,
sizeX: widgetInfo.sizeX, sizeX: widgetInfo.sizeX,
@ -692,6 +692,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
id, id,
tenantId, tenantId,
createdTime, createdTime,
version,
fqn: widgetTypeFqn(widgetInfo.fullFqn), fqn: widgetTypeFqn(widgetInfo.fullFqn),
name: widgetInfo.widgetName, name: widgetInfo.widgetName,
deprecated: widgetInfo.deprecated, deprecated: widgetInfo.deprecated,
@ -701,8 +702,8 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
}; };
export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
createdTime: number): WidgetTypeDetails => { createdTime: number, version: number): WidgetTypeDetails => {
const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime); const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime, version);
return { return {
...widgetTypeEntity, ...widgetTypeEntity,
description: widgetInfo.description, description: widgetInfo.description,

View File

@ -59,12 +59,13 @@ 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, mergeMap, of, Subscription } from 'rxjs'; import { forkJoin, mergeMap, of, Subscription, throwError } from 'rxjs';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
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 { catchError, map, tap } from 'rxjs/operators';
import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models'; import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models';
import { HttpStatusCode } from '@angular/common/http';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
// @dynamic // @dynamic
@ -569,9 +570,12 @@ 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 version = this.widgetTypeDetails?.version ?? null;
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, createdTime).pipe( this.saveWidgetPending = false;
this.widgetService.saveWidgetTypeDetails(this.widget, id, createdTime, version).pipe(
mergeMap((widgetTypeDetails) => { mergeMap((widgetTypeDetails) => {
this.saveWidgetPending = true;
const widgetsBundleId = this.route.snapshot.params.widgetsBundleId as string; const widgetsBundleId = this.route.snapshot.params.widgetsBundleId as string;
if (widgetsBundleId && !id) { if (widgetsBundleId && !id) {
return this.widgetService.addWidgetFqnToWidgetBundle(widgetsBundleId, widgetTypeDetails.fqn).pipe( return this.widgetService.addWidgetFqnToWidgetBundle(widgetsBundleId, widgetTypeDetails.fqn).pipe(
@ -579,7 +583,13 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
); );
} }
return of(widgetTypeDetails); return of(widgetTypeDetails);
}) }),
catchError((err) => {
if (id && err.status === HttpStatusCode.Conflict) {
return this.widgetService.getWidgetTypeById(id.id);
}
return throwError(() => err);
}),
).subscribe({ ).subscribe({
next: (widgetTypeDetails) => { next: (widgetTypeDetails) => {
this.saveWidgetPending = false; this.saveWidgetPending = false;
@ -612,7 +622,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
config.title = this.widget.widgetName; config.title = this.widget.widgetName;
this.widget.defaultConfig = JSON.stringify(config); this.widget.defaultConfig = JSON.stringify(config);
this.isDirty = false; this.isDirty = false;
this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined).pipe( this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined, undefined).pipe(
mergeMap((widget) => { mergeMap((widget) => {
if (saveWidgetAsData.widgetBundleId) { if (saveWidgetAsData.widgetBundleId) {
return this.widgetService.addWidgetFqnToWidgetBundle(saveWidgetAsData.widgetBundleId, widget.fqn).pipe( return this.widgetService.addWidgetFqnToWidgetBundle(saveWidgetAsData.widgetBundleId, widget.fqn).pipe(

View File

@ -16,7 +16,9 @@
--> -->
<mat-toolbar color="primary"> <mat-toolbar color="primary">
<h2 class="main-label">{{ 'entity.version-conflict.label' | translate }}</h2> <h2 class="main-label">
{{ data.message }}
</h2>
<span fxFlex></span> <span fxFlex></span>
<button mat-icon-button <button mat-icon-button
(click)="onCancel()" (click)="onCancel()"
@ -26,23 +28,23 @@
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <div mat-dialog-content>
<div class="message-container"> <div class="message-container">
<span>{{ data.message }}.</span>
<span> <span>
{{ 'entity.version-conflict.link' | translate: {{ 'entity.version-conflict.link' | translate:
{ entityType: (entityTypeTranslations.get(data.entity.id.entityType).type | translate) } { entityType: (entityTypeTranslations.get(data.entity.id.entityType).type | translate) }
}} }}
<a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>. <a class="cursor-pointer" (click)="onLinkClick($event)">{{ 'entity.link' | translate }}</a>.
</span> </span>
<br/>
<span>{{ 'entity.version-conflict.message' | translate }}</span> <span>{{ 'entity.version-conflict.message' | translate }}</span>
</div> </div>
</div> </div>
<div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center"> <div mat-dialog-actions fxLayout="row" fxLayoutAlign="end center">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
(click)="onCancel()" (click)="onDiscard()"
cdkFocusInitial cdkFocusInitial
> >
{{ 'entity.version-conflict.cancel' | translate }} {{ 'entity.version-conflict.discard' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
type="submit" type="submit"

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
$conflict-dialog-width: 700px; $conflict-dialog-width: 530px;
:host { :host {
.main-label { .main-label {

View File

@ -47,6 +47,10 @@ export class EntityConflictDialogComponent {
) {} ) {}
onCancel(): void { onCancel(): void {
this.dialogRef.close();
}
onDiscard(): void {
this.dialogRef.close(false); this.dialogRef.close(false);
} }

View File

@ -24,23 +24,23 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> <mat-progress-bar color="warn" mode="indeterminate" *ngIf="(isLoading$ | async) && !ignoreLoading">
</mat-progress-bar> </mat-progress-bar>
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="(isLoading$ | async) && !ignoreLoading">
<mat-checkbox [formControl]="exportWidgetsFormControl">{{ 'widgets-bundle.export-widgets-bundle-widgets-prompt' | translate }}</mat-checkbox> <mat-checkbox [formControl]="exportWidgetsFormControl">{{ 'widgets-bundle.export-widgets-bundle-widgets-prompt' | translate }}</mat-checkbox>
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary" <button mat-button color="primary"
type="button" type="button"
[disabled]="(isLoading$ | async)" [disabled]="(isLoading$ | async) && !ignoreLoading"
(click)="cancel()"> (click)="cancel()">
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
(click)="export()" (click)="export()"
[disabled]="(isLoading$ | async)"> [disabled]="(isLoading$ | async) && !ignoreLoading">
{{ 'action.export' | translate }} {{ 'action.export' | translate }}
</button> </button>
</div> </div>

View File

@ -27,6 +27,7 @@ import { isDefinedAndNotNull } from '@core/utils';
export interface ExportWidgetsBundleDialogData { export interface ExportWidgetsBundleDialogData {
widgetsBundle: WidgetsBundle; widgetsBundle: WidgetsBundle;
includeBundleWidgetsInExport: boolean; includeBundleWidgetsInExport: boolean;
ignoreLoading?: boolean;
} }
export interface ExportWidgetsBundleDialogResult { export interface ExportWidgetsBundleDialogResult {
@ -44,6 +45,8 @@ export class ExportWidgetsBundleDialogComponent extends DialogComponent<ExportWi
widgetsBundle: WidgetsBundle; widgetsBundle: WidgetsBundle;
ignoreLoading = false;
exportWidgetsFormControl = new FormControl(true); exportWidgetsFormControl = new FormControl(true);
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@ -52,6 +55,7 @@ export class ExportWidgetsBundleDialogComponent extends DialogComponent<ExportWi
public dialogRef: MatDialogRef<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogResult>) { public dialogRef: MatDialogRef<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogResult>) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.widgetsBundle = data.widgetsBundle; this.widgetsBundle = data.widgetsBundle;
this.ignoreLoading = data.ignoreLoading;
if (isDefinedAndNotNull(data.includeBundleWidgetsInExport)) { if (isDefinedAndNotNull(data.includeBundleWidgetsInExport)) {
this.exportWidgetsFormControl.patchValue(data.includeBundleWidgetsInExport, {emitEvent: false}); this.exportWidgetsFormControl.patchValue(data.includeBundleWidgetsInExport, {emitEvent: false});
} }

View File

@ -352,28 +352,7 @@ export class ImportExportService {
forkJoin(tasks).subscribe({ forkJoin(tasks).subscribe({
next: ({includeBundleWidgetsInExport, widgetsBundle}) => { next: ({includeBundleWidgetsInExport, widgetsBundle}) => {
this.dialog.open<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogData, this.handleExportWidgetsBundle(widgetsBundle, includeBundleWidgetsInExport);
ExportWidgetsBundleDialogResult>(ExportWidgetsBundleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
widgetsBundle,
includeBundleWidgetsInExport
}
}).afterClosed().subscribe(
(result) => {
if (result) {
if (includeBundleWidgetsInExport !== result.exportWidgets) {
this.store.dispatch(new ActionPreferencesPutUserSettings({includeBundleWidgetsInExport: result.exportWidgets}));
}
if (result.exportWidgets) {
this.exportWidgetsBundleWithWidgetTypes(widgetsBundle);
} else {
this.exportWidgetsBundleWithWidgetTypeFqns(widgetsBundle);
}
}
}
);
}, },
error: (e) => { error: (e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error'); this.handleExportError(e, 'widgets-bundle.export-failed-error');
@ -401,6 +380,9 @@ export class ImportExportService {
})) }))
.subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name)); .subscribe(ruleChainData => this.exportToPc(ruleChainData, entityData.name));
return; return;
case EntityType.WIDGETS_BUNDLE:
this.exportSelectedWidgetsBundle(entityData as WidgetsBundle);
return;
case EntityType.DASHBOARD: case EntityType.DASHBOARD:
preparedData = this.prepareDashboardExport(entityData as Dashboard); preparedData = this.prepareDashboardExport(entityData as Dashboard);
break; break;
@ -410,6 +392,43 @@ export class ImportExportService {
this.exportToPc(preparedData, entityData.name); this.exportToPc(preparedData, entityData.name);
} }
private exportSelectedWidgetsBundle(widgetsBundle: WidgetsBundle): void {
this.store.pipe(select(selectUserSettingsProperty( 'includeBundleWidgetsInExport'))).pipe(take(1)).subscribe({
next: (includeBundleWidgetsInExport) => {
this.handleExportWidgetsBundle(widgetsBundle, includeBundleWidgetsInExport, true);
},
error: (e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error');
}
});
}
private handleExportWidgetsBundle(widgetsBundle: WidgetsBundle, includeBundleWidgetsInExport: boolean, ignoreLoading?: boolean): void {
this.dialog.open<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogData,
ExportWidgetsBundleDialogResult>(ExportWidgetsBundleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
widgetsBundle,
includeBundleWidgetsInExport,
ignoreLoading
}
}).afterClosed().subscribe(
(result) => {
if (result) {
if (includeBundleWidgetsInExport !== result.exportWidgets) {
this.store.dispatch(new ActionPreferencesPutUserSettings({includeBundleWidgetsInExport: result.exportWidgets}));
}
if (result.exportWidgets) {
this.exportWidgetsBundleWithWidgetTypes(widgetsBundle);
} else {
this.exportWidgetsBundleWithWidgetTypeFqns(widgetsBundle);
}
}
}
);
}
private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) { private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) {
this.widgetService.exportBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe({ this.widgetService.exportBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe({
next: (widgetTypesDetails) => { next: (widgetTypesDetails) => {

View File

@ -41,7 +41,7 @@ import { isNotEmptyStr, mergeDeepIgnoreArray } from '@core/utils';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models'; import { ComponentStyle, Font, TimewindowStyle } from '@shared/models/widget-settings.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { HasTenantId } from '@shared/models/entity.models'; import { HasTenantId, HasVersion } from '@shared/models/entity.models';
import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeysCallbacks, DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models';
@ -199,7 +199,7 @@ export interface WidgetControllerDescriptor {
actionSources?: {[actionSourceId: string]: WidgetActionSource}; actionSources?: {[actionSourceId: string]: WidgetActionSource};
} }
export interface BaseWidgetType extends BaseData<WidgetTypeId>, HasTenantId { export interface BaseWidgetType extends BaseData<WidgetTypeId>, HasTenantId, HasVersion {
tenantId: TenantId; tenantId: TenantId;
fqn: string; fqn: string;
name: string; name: string;

View File

@ -2311,11 +2311,10 @@
"list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }", "list-of-edges": "{ count, plural, =1 {One edge} other {List of # edges} }",
"edge-name-starts-with": "Edges whose names start with '{{prefix}}'", "edge-name-starts-with": "Edges whose names start with '{{prefix}}'",
"version-conflict": { "version-conflict": {
"label": "Version conflict", "message": "Do you want to overwrite existing version or discard changes and load the latest version?",
"message": "Do you want to cancel your changes or overwrite existing version?",
"link": "You can download your version of the {{entityType}} using this", "link": "You can download your version of the {{entityType}} using this",
"overwrite": "Overwrite version", "overwrite": "Overwrite version",
"cancel": "Cancel changes" "discard": "Discard changes"
}, },
"type-tb-resource": "Resource", "type-tb-resource": "Resource",
"type-tb-resources": "Resources", "type-tb-resources": "Resources",