UI: Implement entity version restore

This commit is contained in:
Igor Kulikov 2022-05-26 13:50:50 +03:00
parent 65dc70c51b
commit 46e0c7c908
20 changed files with 347 additions and 66 deletions

View File

@ -190,7 +190,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
SingleEntityVersionLoadRequest versionLoadRequest = (SingleEntityVersionLoadRequest) request;
VersionLoadConfig config = versionLoadRequest.getConfig();
ListenableFuture<EntityExportData> future = gitServiceQueue.getEntity(user.getTenantId(), request.getVersionId(), versionLoadRequest.getExternalEntityId());
Futures.transform(future, entityData -> {
return Futures.transform(future, entityData -> {
EntityImportResult<?> importResult = transactionTemplate.execute(status -> {
try {
return exportImportService.importEntity(user, entityData, EntityImportSettings.builder()

View File

@ -18,7 +18,13 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { combineLatest, Observable, of } from 'rxjs';
import { BranchInfo, EntityVersion, VersionCreateRequest, VersionCreationResult } from '@shared/models/vc.models';
import {
BranchInfo,
EntityVersion,
VersionCreateRequest,
VersionCreationResult,
VersionLoadRequest, VersionLoadResult
} from '@shared/models/vc.models';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { EntityId } from '@shared/models/id/entity-id';
@ -95,4 +101,8 @@ export class EntitiesVersionControlService {
return this.http.get<PageData<EntityVersion>>(`/api/entities/vc/version/${branch}${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
}
public loadEntitiesVersion(request: VersionLoadRequest, config?: RequestConfig): Observable<Array<VersionLoadResult>> {
return this.http.post<Array<VersionLoadResult>>('/api/entities/vc/entity', request, defaultHttpOptionsFromConfig(config));
}
}

View File

@ -157,6 +157,7 @@ import { VersionControlSettingsComponent } from '@home/components/vc/version-con
import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { EntityVersionsTableComponent } from '@home/components/vc/entity-versions-table.component';
import { EntityVersionExportComponent } from '@home/components/vc/entity-version-export.component';
import { EntityVersionRestoreComponent } from '@home/components/vc/entity-version-restore.component';
@NgModule({
declarations:
@ -284,7 +285,8 @@ import { EntityVersionExportComponent } from '@home/components/vc/entity-version
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent,
EntityVersionExportComponent
EntityVersionExportComponent,
EntityVersionRestoreComponent
],
imports: [
CommonModule,
@ -406,7 +408,8 @@ import { EntityVersionExportComponent } from '@home/components/vc/entity-version
VersionControlSettingsComponent,
VersionControlComponent,
EntityVersionsTableComponent,
EntityVersionExportComponent
EntityVersionExportComponent,
EntityVersionRestoreComponent
],
providers: [
WidgetComponentService,

View File

@ -16,45 +16,58 @@
-->
<section style="min-width: 400px;">
<mat-toolbar>
<h2>{{ 'version-control.create-entity-version' | translate }}</h2>
<span fxFlex></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="exportFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-branch-autocomplete
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="exportFormGroup.get('versionName').hasError('required')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || exportFormGroup.invalid || !exportFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
<section *ngIf="!resultMessage">
<mat-toolbar>
<h2>{{ 'version-control.create-entity-version' | translate }}</h2>
<span fxFlex></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="exportFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<tb-branch-autocomplete
required
formControlName="branch">
</tb-branch-autocomplete>
<mat-form-field class="mat-block" fxFlex>
<mat-label translate>version-control.version-name</mat-label>
<input required matInput formControlName="versionName">
<mat-error *ngIf="exportFormGroup.get('versionName').hasError('required')">
{{ 'version-control.version-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-entity-relations' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="export()"
[disabled]="(isLoading$ | async) || exportFormGroup.invalid || !exportFormGroup.dirty">
{{ 'action.create' | translate }}
</button>
</div>
</section>
<section *ngIf="resultMessage">
<div class="mat-title export-result-message">{{ resultMessage }}</div>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.close' | translate }}
</button>
</div>
</section>
</section>

View File

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2022 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.
*/
:host {
.export-result-message {
padding: 48px 8px 8px;
text-align: center;
}
}

View File

@ -26,11 +26,12 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { EntityId } from '@shared/models/id/entity-id';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-entity-version-export',
templateUrl: './entity-version-export.component.html',
styleUrls: []
styleUrls: ['./entity-version-export.component.scss']
})
export class EntityVersionExportComponent extends PageComponent implements OnInit {
@ -43,10 +44,16 @@ export class EntityVersionExportComponent extends PageComponent implements OnIni
@Input()
onClose: (result: VersionCreationResult | null, branch: string | null) => void;
@Input()
onContentUpdated: () => void;
exportFormGroup: FormGroup;
resultMessage: string;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private translate: TranslateService,
private fb: FormBuilder) {
super(store);
}
@ -76,7 +83,12 @@ export class EntityVersionExportComponent extends PageComponent implements OnIni
type: VersionCreateRequestType.SINGLE_ENTITY
};
this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => {
if (this.onClose) {
if (!result.added && !result.modified) {
this.resultMessage = this.translate.instant('version-control.nothing-to-commit');
if (this.onContentUpdated) {
this.onContentUpdated();
}
} else if (this.onClose) {
this.onClose(result, request.branch);
}
});

View File

@ -0,0 +1,49 @@
<!--
Copyright © 2016-2022 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<section style="min-width: 400px;">
<mat-toolbar>
<h2>{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}</h2>
<span fxFlex></span>
</mat-toolbar>
<mat-progress-bar color="warn" style="z-index: 10; width: 100%; margin-bottom: -4px;" mode="indeterminate"
*ngIf="isLoading$ | async">
</mat-progress-bar>
<form [formGroup]="restoreFormGroup" style="padding-top: 16px;">
<fieldset [disabled]="isLoading$ | async">
<div fxFlex fxLayout="column">
<mat-checkbox formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-entity-relations' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>
<div fxLayoutAlign="end center" fxLayoutGap="8px">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="restore()"
[disabled]="(isLoading$ | async) || restoreFormGroup.invalid">
{{ 'action.restore' | translate }}
</button>
</div>
</section>

View File

@ -0,0 +1,86 @@
///
/// Copyright © 2016-2022 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, Input, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { SingleEntityVersionLoadRequest, VersionLoadRequestType, VersionLoadResult } from '@shared/models/vc.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { EntityId } from '@shared/models/id/entity-id';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-entity-version-restore',
templateUrl: './entity-version-restore.component.html',
styleUrls: []
})
export class EntityVersionRestoreComponent extends PageComponent implements OnInit {
@Input()
branch: string;
@Input()
versionName: string;
@Input()
versionId: string;
@Input()
externalEntityId: EntityId;
@Input()
onClose: (result: Array<VersionLoadResult> | null) => void;
restoreFormGroup: FormGroup;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private translate: TranslateService,
private fb: FormBuilder) {
super(store);
}
ngOnInit(): void {
this.restoreFormGroup = this.fb.group({
loadRelations: [false, []]
});
}
cancel(): void {
if (this.onClose) {
this.onClose(null);
}
}
restore(): void {
const request: SingleEntityVersionLoadRequest = {
branch: this.branch,
versionId: this.versionId,
externalEntityId: this.externalEntityId,
config: {
loadRelations: this.restoreFormGroup.get('loadRelations').value
},
type: VersionLoadRequestType.SINGLE_ENTITY
};
this.entitiesVersionControlService.loadEntitiesVersion(request).subscribe((result) => {
if (this.onClose) {
this.onClose(result);
}
});
}
}

View File

@ -99,6 +99,21 @@
{{ entityVersion.name }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef style="min-width: 40px; max-width: 40px; width: 40px">
</mat-header-cell>
<mat-cell *matCellDef="let entityVersion">
<div fxFlex fxLayout="row" fxLayoutAlign="end">
<button *ngIf="singleEntityMode" mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ 'version-control.restore-version' | translate }}"
matTooltipPosition="above"
#restoreVersionButton
(click)="toggleRestoreEntityVersion($event, restoreVersionButton, entityVersion)">
<mat-icon>restore</mat-icon>
</button>
</div>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row [fxShow]="!dataSource.dataLoading" *matRowDef="let entityVersion; columns: displayedColumns;"></mat-row>
</table>

View File

@ -18,10 +18,10 @@ import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
ElementRef, EventEmitter,
Input,
OnDestroy,
OnInit, Renderer2,
OnInit, Output, Renderer2,
ViewChild, ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
@ -33,7 +33,7 @@ import { BehaviorSubject, fromEvent, merge, Observable, of, ReplaySubject } from
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { PageLink } from '@shared/models/page/page-link';
import { catchError, debounceTime, distinctUntilChanged, map, tap } from 'rxjs/operators';
import { EntityVersion, VersionCreationResult } from '@shared/models/vc.models';
import { EntityVersion, VersionCreationResult, VersionLoadResult } from '@shared/models/vc.models';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
@ -46,6 +46,7 @@ import { TbPopoverService } from '@shared/components/popover.service';
import { EntityVersionExportComponent } from '@home/components/vc/entity-version-export.component';
import { MatButton } from '@angular/material/button';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { EntityVersionRestoreComponent } from '@home/components/vc/entity-version-restore.component';
@Component({
selector: 'tb-entity-versions-table',
@ -59,7 +60,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
@Input()
singleEntityMode = false;
displayedColumns = ['timestamp', 'id', 'name'];
displayedColumns = ['timestamp', 'id', 'name', 'actions'];
pageLink: PageLink;
textSearchMode = false;
dataSource: EntityVersionsDatasource;
@ -73,8 +74,6 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
viewsInited = false;
vcExportPopover: TbPopoverComponent;
private componentResize$: ResizeObserver;
@Input()
@ -104,6 +103,9 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
@Input()
entityId: EntityId;
@Output()
versionRestored = new EventEmitter<void>();
@ViewChild('searchInput') searchInputField: ElementRef;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ -182,13 +184,13 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
this.vcExportPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, EntityVersionExportComponent, 'bottom', true, null,
const vcExportPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, EntityVersionExportComponent, 'left', true, null,
{
branch: this.branch,
entityId: this.entityId,
onClose: (result: VersionCreationResult | null, branch: string | null) => {
this.vcExportPopover.hide();
vcExportPopover.hide();
if (result) {
if (this.branch !== branch) {
this.branchChanged(branch);
@ -196,13 +198,39 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
this.updateData();
}
}
},
onContentUpdated: () => {
vcExportPopover.updatePosition();
setTimeout(() => {
vcExportPopover.updatePosition();
});
}
}, {}, {}, {}, false);
}
}
toggleRestoreEntityVersion($event: Event, restoreVersionButton: MatButton, entityVersion: EntityVersion) {
if ($event) {
$event.stopPropagation();
}
const trigger = restoreVersionButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const restoreVersionPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, EntityVersionRestoreComponent, 'left', true, null,
{
branch: this.branch,
versionName: entityVersion.name,
versionId: entityVersion.id,
externalEntityId: this.externalEntityIdValue,
onClose: (result: Array<VersionLoadResult> | null) => {
restoreVersionPopover.hide();
if (result && result.length) {
this.versionRestored.emit();
}
}
}, {}, {}, {}, false);
this.vcExportPopover.tbVisibleChange.subscribe((visible: boolean) => {
if (!visible) {
this.vcExportPopover = null;
}
});
}
}

View File

@ -22,5 +22,6 @@
<tb-entity-versions-table [singleEntityMode]="singleEntityMode"
[active]="active"
[entityId]="entityId"
[externalEntityId]="externalEntityId"></tb-entity-versions-table>
[externalEntityId]="externalEntityId"
(versionRestored)="versionRestored.emit()"></tb-entity-versions-table>
</ng-template>

View File

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { selectHasVersionControl } from '@core/auth/auth.selectors';
@ -47,6 +47,9 @@ export class VersionControlComponent implements OnInit, HasConfirmForm {
@Input()
entityId: EntityId;
@Output()
versionRestored = new EventEmitter<void>();
hasVersionControl$ = this.store.pipe(select(selectHasVersionControl));
constructor(private store: Store<AppState>) {

View File

@ -52,5 +52,6 @@
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

View File

@ -52,5 +52,6 @@
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

View File

@ -22,5 +22,6 @@
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

View File

@ -75,8 +75,9 @@
label="{{ 'audit-log.audit-logs' | translate }}">
<tb-audit-log-table detailsMode="true" [active]="auditLogsTab.isActive" [auditLogMode]="auditLogModes.ENTITY" [entityId]="entity.id"></tb-audit-log-table>
</mat-tab>
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN && !isEdit"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

View File

@ -52,5 +52,6 @@
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

View File

@ -55,5 +55,6 @@
<mat-tab *ngIf="entity && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

View File

@ -15,16 +15,26 @@
///
import { EntityId } from '@shared/models/id/entity-id';
import { EntityType } from '@shared/models/entity-type.models';
export interface VersionCreateConfig {
saveRelations: boolean;
}
export interface VersionLoadConfig {
loadRelations: boolean;
}
export enum VersionCreateRequestType {
SINGLE_ENTITY = 'SINGLE_ENTITY',
COMPLEX = 'COMPLEX'
}
export enum VersionLoadRequestType {
SINGLE_ENTITY = 'SINGLE_ENTITY',
ENTITY_TYPE = 'ENTITY_TYPE'
}
export interface VersionCreateRequest {
versionName: string;
branch: string;
@ -37,6 +47,18 @@ export interface SingleEntityVersionCreateRequest extends VersionCreateRequest {
type: VersionCreateRequestType.SINGLE_ENTITY;
}
export interface VersionLoadRequest {
branch: string;
versionId: string;
type: VersionLoadRequestType;
}
export interface SingleEntityVersionLoadRequest extends VersionLoadRequest {
externalEntityId: EntityId;
config: VersionLoadConfig;
type: VersionLoadRequestType.SINGLE_ENTITY;
}
export interface BranchInfo {
name: string;
default: boolean;
@ -54,3 +76,10 @@ export interface VersionCreationResult {
modified: number;
removed: number;
}
export interface VersionLoadResult {
entityType: EntityType;
created: number;
updated: number;
deleted: number;
}

View File

@ -57,7 +57,8 @@
"download": "Download",
"next-with-label": "Next: {{label}}",
"read-more": "Read more",
"hide": "Hide"
"hide": "Hide",
"restore": "Restore"
},
"aggregation": {
"aggregation": "Aggregation",
@ -3126,7 +3127,11 @@
"no-entity-versions-text": "No entity versions found",
"no-versions-text": "No versions found",
"copy-full-version-id": "Copy full version id",
"create-version": "Create version"
"create-version": "Create version",
"nothing-to-commit": "Nothing to commit",
"restore-version": "Restore version",
"restore-entity-from-version": "Restore entity from version '{{versionName}}'",
"load-entity-relations": "Load entity relations"
},
"widget": {
"widget-library": "Widgets Library",