UI: Implement rule chain page version control.

This commit is contained in:
Igor Kulikov 2022-06-01 18:02:52 +03:00
parent 8382850d1a
commit 14023e4cc9
9 changed files with 148 additions and 27 deletions

View File

@ -27,6 +27,7 @@ import { AppState } from '@core/core.state';
import { EntitiesVersionControlService } from '@core/http/entities-version-control.service'; import { EntitiesVersionControlService } from '@core/http/entities-version-control.service';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
@Component({ @Component({
selector: 'tb-entity-version-create', selector: 'tb-entity-version-create',
@ -50,6 +51,9 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
@Input() @Input()
onContentUpdated: () => void; onContentUpdated: () => void;
@Input()
onBeforeCreateVersion: () => Observable<any>;
createVersionFormGroup: FormGroup; createVersionFormGroup: FormGroup;
resultMessage: string; resultMessage: string;
@ -78,25 +82,29 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
} }
export(): void { export(): void {
const request: SingleEntityVersionCreateRequest = { const before = this.onBeforeCreateVersion ? this.onBeforeCreateVersion() : of(null);
entityId: this.entityId, before.subscribe(() => {
branch: this.createVersionFormGroup.get('branch').value, const request: SingleEntityVersionCreateRequest = {
versionName: this.createVersionFormGroup.get('versionName').value, entityId: this.entityId,
config: { branch: this.createVersionFormGroup.get('branch').value,
saveRelations: this.createVersionFormGroup.get('saveRelations').value, versionName: this.createVersionFormGroup.get('versionName').value,
saveAttributes: this.createVersionFormGroup.get('saveAttributes').value, config: {
}, saveRelations: this.createVersionFormGroup.get('saveRelations').value,
type: VersionCreateRequestType.SINGLE_ENTITY saveAttributes: this.createVersionFormGroup.get('saveAttributes').value,
}; },
this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => { type: VersionCreateRequestType.SINGLE_ENTITY
if (!result.added && !result.modified) { };
this.resultMessage = this.translate.instant('version-control.nothing-to-commit'); this.entitiesVersionControlService.saveEntitiesVersion(request).subscribe((result) => {
if (this.onContentUpdated) { if (!result.added && !result.modified) {
this.onContentUpdated(); this.resultMessage = this.translate.instant('version-control.nothing-to-commit');
if (this.onContentUpdated) {
this.onContentUpdated();
}
} else if (this.onClose) {
this.onClose(result, request.branch);
} }
} else if (this.onClose) { });
this.onClose(result, request.branch);
}
}); });
} }
} }

View File

@ -16,7 +16,7 @@
--> -->
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="mat-padding tb-entity-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="mat-elevation-z1 tb-entity-table-content"> <div fxFlex fxLayout="column" class="tb-entity-table-content" [ngClass]="{'mat-elevation-z1': !popoverMode}">
<mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
@ -45,6 +45,11 @@
<mat-icon>update</mat-icon> <mat-icon>update</mat-icon>
{{'version-control.create-entities-version' | translate }} {{'version-control.create-entities-version' | translate }}
</button> </button>
<button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon>refresh</mat-icon>
</button>
<button mat-icon-button <button mat-icon-button
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="enterFilterMode()" (click)="enterFilterMode()"

View File

@ -62,6 +62,12 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
@Input() @Input()
singleEntityMode = false; singleEntityMode = false;
@Input()
popoverMode = false;
@Input()
onBeforeCreateVersion: () => Observable<any>;
displayedColumns = ['timestamp', 'id', 'name', 'author', 'actions']; displayedColumns = ['timestamp', 'id', 'name', 'author', 'actions'];
pageLink: PageLink; pageLink: PageLink;
textSearchMode = false; textSearchMode = false;
@ -195,6 +201,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
branch: this.branch, branch: this.branch,
entityId: this.entityId, entityId: this.entityId,
entityName: this.entityName, entityName: this.entityName,
onBeforeCreateVersion: this.onBeforeCreateVersion,
onClose: (result: VersionCreationResult | null, branch: string | null) => { onClose: (result: VersionCreationResult | null, branch: string | null) => {
createVersionPopover.hide(); createVersionPopover.hide();
if (result) { if (result) {
@ -353,7 +360,7 @@ export class EntityVersionsTableComponent extends PageComponent implements OnIni
} }
} }
private updateData() { updateData() {
this.pageLink.page = this.paginator.pageIndex; this.pageLink.page = this.paginator.pageIndex;
this.pageLink.pageSize = this.paginator.pageSize; this.pageLink.pageSize = this.paginator.pageSize;
this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.property = this.sort.active;

View File

@ -20,6 +20,8 @@
</tb-repository-settings> </tb-repository-settings>
<ng-template #versionsTable> <ng-template #versionsTable>
<tb-entity-versions-table [singleEntityMode]="singleEntityMode" <tb-entity-versions-table [singleEntityMode]="singleEntityMode"
[popoverMode]="popoverMode"
[onBeforeCreateVersion]="onBeforeCreateVersion"
[active]="active" [active]="active"
[entityId]="entityId" [entityId]="entityId"
[entityName]="entityName" [entityName]="entityName"

View File

@ -22,6 +22,7 @@ import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component'; import { RepositorySettingsComponent } from '@home/components/vc/repository-settings.component';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'tb-version-control', selector: 'tb-version-control',
@ -35,6 +36,9 @@ export class VersionControlComponent implements OnInit, HasConfirmForm {
@Input() @Input()
detailsMode = false; detailsMode = false;
@Input()
popoverMode = false;
@Input() @Input()
active = true; active = true;
@ -50,6 +54,9 @@ export class VersionControlComponent implements OnInit, HasConfirmForm {
@Input() @Input()
entityName: string; entityName: string;
@Input()
onBeforeCreateVersion: () => Observable<any>;
@Output() @Output()
versionRestored = new EventEmitter<void>(); versionRestored = new EventEmitter<void>();

View File

@ -164,6 +164,17 @@
</tb-details-panel> </tb-details-panel>
</mat-drawer> </mat-drawer>
<mat-drawer-content class="tb-rulechain-graph-content"> <mat-drawer-content class="tb-rulechain-graph-content">
<button #versionControlButton
*ngIf="!isImport"
mat-button
color="primary"
type="button"
mat-icon-button class="mat-fab version-control-button"
(click)="toggleVersionControl($event, versionControlButton)"
matTooltip="{{'version-control.version-control' | translate}}"
matTooltipPosition="above">
<mat-icon>history</mat-icon>
</button>
<button mat-button <button mat-button
type="button" type="button"
mat-icon-button class="tb-fullscreen-button" mat-icon-button class="tb-fullscreen-button"

View File

@ -44,6 +44,21 @@
margin: 0 6px; margin: 0 6px;
z-index: 2; z-index: 2;
} }
button.mat-button.mat-icon-button.version-control-button {
position: absolute;
top: 10px;
right: 60px;
opacity: .85;
margin: 0 6px;
z-index: 2;
&.mat-fab {
.mat-button-wrapper {
padding: 0;
}
}
}
section.tb-header-buttons.tb-library-open { section.tb-header-buttons.tb-library-open {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@ -17,15 +17,15 @@
import { import {
AfterViewInit, AfterViewInit,
Component, Component,
ElementRef, ElementRef, EventEmitter,
HostBinding, HostBinding,
Inject, Inject,
OnDestroy, OnDestroy,
OnInit, OnInit,
QueryList, QueryList, Renderer2,
SkipSelf, SkipSelf,
ViewChild, ViewChild,
ViewChildren, ViewChildren, ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -63,7 +63,7 @@ import {
} from '@shared/models/rule-node.models'; } from '@shared/models/rule-node.models';
import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models'; import { FcRuleNodeModel, FcRuleNodeTypeModel, RuleChainMenuContextInfo } from './rulechain-page.models';
import { RuleChainService } from '@core/http/rule-chain.service'; import { RuleChainService } from '@core/http/rule-chain.service';
import { fromEvent, NEVER, Observable, of, Subscription } from 'rxjs'; import { fromEvent, NEVER, Observable, of, ReplaySubject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, mergeMap, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, mergeMap, tap } from 'rxjs/operators';
import { ISearchableComponent } from '../../models/searchable-component.models'; import { ISearchableComponent } from '../../models/searchable-component.models';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
@ -75,6 +75,11 @@ import { ItemBufferService, RuleNodeConnection } from '@core/services/item-buffe
import { Hotkey } from 'angular2-hotkeys'; import { Hotkey } from 'angular2-hotkeys';
import { DebugEventType, EventType } from '@shared/models/event.models'; import { DebugEventType, EventType } from '@shared/models/event.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { EntityVersionCreateComponent } from '@home/components/vc/entity-version-create.component';
import { VersionCreationResult } from '@shared/models/vc.models';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
@Component({ @Component({
selector: 'tb-rulechain-page', selector: 'tb-rulechain-page',
@ -231,6 +236,8 @@ export class RuleChainPageComponent extends PageComponent
flowchartConstants = FlowchartConstants; flowchartConstants = FlowchartConstants;
updateBreadcrumbs = new EventEmitter();
private rxSubscription: Subscription; private rxSubscription: Subscription;
private tooltipTimeout: Timeout; private tooltipTimeout: Timeout;
@ -242,11 +249,13 @@ export class RuleChainPageComponent extends PageComponent
private authService: AuthService, private authService: AuthService,
private translate: TranslateService, private translate: TranslateService,
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
public dialog: MatDialog, public dialog: MatDialog,
public dialogService: DialogService, public dialogService: DialogService,
public fb: FormBuilder) { public fb: FormBuilder) {
super(store); super(store);
this.rxSubscription = this.route.data.subscribe( this.rxSubscription = this.route.data.subscribe(
() => { () => {
this.reset(); this.reset();
@ -1376,7 +1385,8 @@ export class RuleChainPageComponent extends PageComponent
}, 0); }, 0);
} }
saveRuleChain() { saveRuleChain(): Observable<any> {
const saveResult = new ReplaySubject();
let saveRuleChainObservable: Observable<RuleChain>; let saveRuleChainObservable: Observable<RuleChain>;
if (this.isImport) { if (this.isImport) {
saveRuleChainObservable = this.ruleChainService.saveRuleChain(this.ruleChain); saveRuleChainObservable = this.ruleChainService.saveRuleChain(this.ruleChain);
@ -1442,6 +1452,20 @@ export class RuleChainPageComponent extends PageComponent
} else { } else {
this.createRuleChainModel(); this.createRuleChainModel();
} }
saveResult.next();
});
});
return saveResult;
}
reloadRuleChain() {
this.ruleChainService.getRuleChain(this.ruleChain.id.id).subscribe((ruleChain) => {
this.ruleChain = ruleChain;
this.updateBreadcrumbs.emit();
this.ruleChainService.getRuleChainMetadata(this.ruleChain.id.id).subscribe((ruleChainMetaData) => {
this.ruleChainMetaData = ruleChainMetaData;
this.isDirtyValue = false;
this.createRuleChainModel();
}); });
}); });
} }
@ -1509,6 +1533,38 @@ export class RuleChainPageComponent extends PageComponent
}).afterClosed(); }).afterClosed();
} }
toggleVersionControl($event: Event, versionControlButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = versionControlButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const versionControlPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, VersionControlComponent, 'leftTop', true, null,
{
detailsMode: true,
popoverMode: true,
active: true,
singleEntityMode: true,
externalEntityId: this.ruleChain.externalId || this.ruleChain.id,
entityId: this.ruleChain.id,
entityName: this.ruleChain.name,
onBeforeCreateVersion: () => {
if (this.isDirty) {
return this.saveRuleChain();
} else {
return of(null);
}
}
}, {width: '800px', height: '600px'}, {}, {}, true);
versionControlPopover.tbComponentRef.instance.versionRestored.subscribe(() => {
this.reloadRuleChain();
});
}
}
private updateNodeErrorTooltip(node: FcRuleNode) { private updateNodeErrorTooltip(node: FcRuleNode) {
if (node.error) { if (node.error) {
const element = $('#' + node.id); const element = $('#' + node.id);

View File

@ -15,7 +15,7 @@
/// ///
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Subject } from 'rxjs'; import { BehaviorSubject, Subject, Subscription } from 'rxjs';
import { BreadCrumb, BreadCrumbConfig } from './breadcrumb'; import { BreadCrumb, BreadCrumbConfig } from './breadcrumb';
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router';
import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { distinctUntilChanged, filter, map } from 'rxjs/operators';
@ -32,10 +32,20 @@ import { BroadcastService } from '@core/services/broadcast.service';
export class BreadcrumbComponent implements OnInit, OnDestroy { export class BreadcrumbComponent implements OnInit, OnDestroy {
activeComponentValue: any; activeComponentValue: any;
updateBreadcrumbsSubscription: Subscription = null;
@Input() @Input()
set activeComponent(activeComponent: any) { set activeComponent(activeComponent: any) {
if (this.updateBreadcrumbsSubscription) {
this.updateBreadcrumbsSubscription.unsubscribe();
this.updateBreadcrumbsSubscription = null;
}
this.activeComponentValue = activeComponent; this.activeComponentValue = activeComponent;
if (this.activeComponentValue && this.activeComponentValue.updateBreadcrumbs) {
this.updateBreadcrumbsSubscription = this.activeComponentValue.updateBreadcrumbs.subscribe(() => {
this.breadcrumbs$.next(this.buildBreadCrumbs(this.activatedRoute.snapshot));
});
}
} }
breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot)); breadcrumbs$: Subject<Array<BreadCrumb>> = new BehaviorSubject<Array<BreadCrumb>>(this.buildBreadCrumbs(this.activatedRoute.snapshot));