FE-821: Added domain object review module (#76)

This commit is contained in:
Ildar Galeev 2019-04-19 15:00:48 +03:00 committed by GitHub
parent 20a4485903
commit 9b6fa61686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 339 additions and 111 deletions

View File

@ -0,0 +1,14 @@
import { Reference } from '../gen-damsel/domain';
import { MetaStruct, MetaUnion } from '../damsel-meta';
export interface ModificationItem {
monacoContent: string;
meta: MetaStruct | MetaUnion;
}
export interface DomainModificationModel {
ref: Reference;
objectType: string;
original: ModificationItem;
modified: ModificationItem;
}

View File

@ -1,26 +1,30 @@
<cc-card-container> <div class="editor-container">
<div>
<button mat-button routerLink="/domain">
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
BACK TO DOMAIN
</button>
</div>
<div *ngIf="isLoading" fxLayout fxLayoutAlign="center stretch"><mat-spinner></mat-spinner></div> <div *ngIf="isLoading" fxLayout fxLayoutAlign="center stretch"><mat-spinner></mat-spinner></div>
<mat-card *ngIf="initialized"> <mat-card *ngIf="initialized">
<mat-card-header> <mat-card-header>
<mat-card-subtitle>{{ objectType }}</mat-card-subtitle> <mat-card-title>Edit {{ model.objectType }}</mat-card-title>
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<cc-monaco-editor <cc-monaco-editor
[file]="file" class="editor"
[file]="modifiedFile"
(fileChange)="fileChange($event)" (fileChange)="fileChange($event)"
[options]="options"
[codeLensProviders]="codeLensProviders" [codeLensProviders]="codeLensProviders"
[completionProviders]="completionProviders" [completionProviders]="completionProviders"
></cc-monaco-editor> ></cc-monaco-editor>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<button mat-button [disabled]="!valid">COMMIT</button> <div fxLayout="row" fxLayoutAlign="space-between center">
<button mat-button routerLink="/domain">
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
BACK TO DOMAIN
</button>
<button mat-button color="warn" (click)="resetChanges()">RESET CHANGES</button>
<button mat-button (click)="reviewChanges()">
REVIEW CHANGES
<mat-icon aria-label="Login">keyboard_arrow_right</mat-icon>
</button>
</div>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</cc-card-container> </div>

View File

@ -1,4 +0,0 @@
cc-monaco-editor {
display: block;
height: calc(100vh - 250px);
}

View File

@ -1,69 +1,88 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { import { MonacoFile, CodeLensProvider, CompletionProvider } from '../../monaco-editor/model';
IEditorOptions,
MonacoFile,
CodeLensProvider,
CompletionProvider
} from '../../monaco-editor/model';
import { DomainObjModificationService } from './domain-obj-modification.service'; import { DomainObjModificationService } from './domain-obj-modification.service';
import { DomainObjCodeLensProvider } from './domain-obj-code-lens-provider'; import { DomainObjCodeLensProvider } from './domain-obj-code-lens-provider';
import { DomainObjCompletionProvider } from './domain-obj-completion-provider'; import { DomainObjCompletionProvider } from './domain-obj-completion-provider';
import { DomainReviewService } from '../domain-review.service';
import { toMonacoFile } from '../utils';
import { DomainModificationModel } from '../domain-modification-model';
@Component({ @Component({
templateUrl: './domain-obj-modification.component.html', templateUrl: './domain-obj-modification.component.html',
styleUrls: ['domain-obj-modification.component.scss'], styleUrls: ['../editor-container.scss'],
providers: [DomainObjModificationService] providers: [DomainObjModificationService]
}) })
export class DomainObjModificationComponent implements OnInit { export class DomainObjModificationComponent implements OnInit, OnDestroy {
initialized = false; initialized = false;
isLoading: boolean; isLoading: boolean;
file: MonacoFile;
options: IEditorOptions = {
readOnly: false
};
objectType: string;
valid = false; valid = false;
codeLensProviders: CodeLensProvider[]; codeLensProviders: CodeLensProvider[];
completionProviders: CompletionProvider[]; completionProviders: CompletionProvider[];
modifiedFile: MonacoFile;
private model: DomainModificationModel;
private initSub: Subscription;
constructor( constructor(
private router: Router,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private domainObjModificationService: DomainObjModificationService private domainObjModService: DomainObjModificationService,
) { private domainReviewService: DomainReviewService
this.domainObjModificationService.valueValid.subscribe(v => (this.valid = v)); ) {}
}
ngOnInit() { ngOnInit() {
this.initialize(); this.initSub = this.initialize();
this.codeLensProviders = [new DomainObjCodeLensProvider()]; this.codeLensProviders = [new DomainObjCodeLensProvider()];
this.completionProviders = [new DomainObjCompletionProvider()]; this.completionProviders = [new DomainObjCompletionProvider()];
} }
fileChange({ content }: MonacoFile) { ngOnDestroy() {
const meta = this.domainObjModificationService.applyValue(content); if (this.initSub) {
console.log('BUILDED', meta); this.initSub.unsubscribe();
}
} }
private initialize() { fileChange({ content }: MonacoFile) {
const { valid, payload } = this.domainObjModService.applyValue(
this.model.modified.meta,
content
);
this.valid = valid;
if (valid) {
this.model.modified = {
meta: payload,
monacoContent: content
};
}
}
reviewChanges() {
this.domainReviewService.addReviewModel(this.model);
this.router.navigate(['domain', JSON.stringify(this.model.ref), 'review']);
}
resetChanges() {
this.model = this.domainObjModService.reset(this.model);
this.modifiedFile = toMonacoFile(this.model.modified.monacoContent);
}
private initialize(): Subscription {
this.isLoading = true; this.isLoading = true;
this.domainObjModificationService.initialize().subscribe( return this.domainObjModService.init().subscribe(
({ file, objectType }) => { model => {
this.isLoading = false; this.isLoading = false;
this.file = file; this.model = model;
this.objectType = objectType; this.modifiedFile = toMonacoFile(model.modified.monacoContent);
this.initialized = true; this.initialized = true;
}, },
err => { err => {
console.error(err); console.error(err);
this.isLoading = false; this.isLoading = false;
this.snackBar this.snackBar.open(`An error occurred while initializing: ${err}`, 'OK');
.open(`An error occurred while initializing: ${err}`, 'RETRY', {
duration: 10000
})
.onAction()
.subscribe(() => this.initialize());
} }
); );
} }

View File

@ -1,39 +1,34 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Observable, combineLatest, Subject, BehaviorSubject } from 'rxjs'; import { Observable, combineLatest, Subject, of } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import cloneDeep from 'lodash-es/cloneDeep';
import { DomainObject, Reference } from '../../gen-damsel/domain'; import { DomainObject, Reference } from '../../gen-damsel/domain';
import { MetadataService } from '../metadata.service'; import { MetadataService } from '../metadata.service';
import { DomainService } from '../domain.service'; import { DomainService } from '../domain.service';
import { toJson } from '../../shared/thrift-json-converter';
import { extract } from '../../shared/thrift-utils';
import { MonacoFile } from '../../monaco-editor/model';
import { MetaBuilder } from '../../damsel-meta/meta-builder.service'; import { MetaBuilder } from '../../damsel-meta/meta-builder.service';
import { MetaStruct, MetaUnion } from '../../damsel-meta/model'; import { MetaStruct, MetaUnion, MetaPayload } from '../../damsel-meta/model';
import { ModificationPayload } from './modification-payload';
import { MetaApplicator } from '../../damsel-meta/meta-applicator.service'; import { MetaApplicator } from '../../damsel-meta/meta-applicator.service';
import { toMonacoContent, parseRef } from '../utils';
import { DomainReviewService } from '../domain-review.service';
import { DomainModificationModel } from '../domain-modification-model';
@Injectable() @Injectable()
export class DomainObjModificationService { export class DomainObjModificationService {
private meta: MetaStruct | MetaUnion;
private errors$: Subject<string> = new Subject(); private errors$: Subject<string> = new Subject();
private valueValid$: Subject<boolean> = new BehaviorSubject(false);
get errors(): Observable<string> { get errors(): Observable<string> {
return this.errors$; return this.errors$;
} }
get valueValid(): Observable<boolean> {
return this.valueValid$;
}
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private domainService: DomainService, private domainService: DomainService,
private metadataService: MetadataService, private metadataService: MetadataService,
private metaBuilder: MetaBuilder, private metaBuilder: MetaBuilder,
private metaApplicator: MetaApplicator private metaApplicator: MetaApplicator,
private domainReviewService: DomainReviewService
) { ) {
this.metaBuilder.errors.subscribe(e => { this.metaBuilder.errors.subscribe(e => {
this.errors$.next(e); this.errors$.next(e);
@ -42,29 +37,40 @@ export class DomainObjModificationService {
this.metaApplicator.errors.subscribe(e => console.log('Apply meta error:', e)); this.metaApplicator.errors.subscribe(e => console.log('Apply meta error:', e));
} }
initialize(namespace = 'domain'): Observable<ModificationPayload> { init(namespace = 'domain'): Observable<DomainModificationModel> {
return this.route.params.pipe( return combineLatest(this.route.params, this.domainReviewService.reviewModel).pipe(
map(({ ref }) => this.parseParams(ref)), switchMap(([routeParams, model]) => {
switchMap(ref => if (model && JSON.stringify(model.ref) === routeParams.ref) {
combineLatest( return of(model);
}
const ref = parseRef(routeParams.ref);
return combineLatest(
this.metadataService.getDomainObjectType(ref), this.metadataService.getDomainObjectType(ref),
this.domainService.getDomainObject(ref) this.domainService.getDomainObject(ref)
) ).pipe(
), switchMap(([objectType, domainObj]) =>
switchMap(([objectType, domainObj]) => this.buildMeta(objectType, domainObj, namespace)) this.build(ref, objectType, domainObj, namespace)
)
);
})
); );
} }
applyValue(json: string): MetaStruct | MetaUnion | null { applyValue(meta: MetaStruct | MetaUnion, json: string): MetaPayload {
if (!this.meta) { return this.metaApplicator.apply(meta, json);
throw new Error('Service is not initialized');
}
const result = this.metaApplicator.apply(this.meta, json);
this.valueValid$.next(result.valid);
return result.payload;
} }
private buildMeta(objectType, domainObj, namespace) { reset(model: DomainModificationModel): DomainModificationModel {
model.modified = cloneDeep(model.original);
return model;
}
private build(
ref: Reference,
objectType: string,
domainObj: DomainObject,
namespace: string
): Observable<DomainModificationModel> {
if (!objectType) { if (!objectType) {
throw new Error('Domain object type not found'); throw new Error('Domain object type not found');
} }
@ -72,32 +78,26 @@ export class DomainObjModificationService {
throw new Error('Domain object not found'); throw new Error('Domain object not found');
} }
return this.metaBuilder.build(objectType, namespace).pipe( return this.metaBuilder.build(objectType, namespace).pipe(
tap(({ payload, valid }) => { map(({ payload, valid }) => {
if (!valid) { if (!valid) {
throw new Error('Build meta failed'); throw new Error('Build initial meta failed');
} }
this.meta = payload; const monacoContent = toMonacoContent(domainObj);
}), const applyResult = this.metaApplicator.apply(payload, monacoContent);
map(() => ({ if (!applyResult.valid) {
file: this.toMonacoFile(domainObj), throw new Error('Apply original value failed');
objectType }
})) const reviewItem = {
monacoContent,
meta: applyResult.payload
};
return {
ref,
original: cloneDeep(reviewItem),
modified: reviewItem,
objectType
};
})
); );
} }
private parseParams(ref: string): Reference {
try {
return JSON.parse(ref);
} catch {
throw new Error('Malformed domain object ref');
}
}
private toMonacoFile(domainObj: DomainObject | null): MonacoFile {
return {
uri: 'index.json',
language: 'json',
content: JSON.stringify(toJson(extract(domainObj)), null, 2)
};
}
} }

View File

@ -1,6 +0,0 @@
import { MonacoFile } from '../../monaco-editor/model';
export interface ModificationPayload {
file: MonacoFile;
objectType: string;
}

View File

@ -0,0 +1,40 @@
<div class="editor-container">
<mat-card *ngIf="initialized">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between stretch">
<mat-card-title>Review changes of {{ objectType }}</mat-card-title>
<div>
<mat-checkbox
[checked]="options.renderSideBySide"
(change)="renderSideBySide($event)"
>Side by side</mat-checkbox
>
</div>
</mat-card-header>
<mat-card-content>
<cc-monaco-diff-editor
class="editor"
[options]="options"
[original]="original"
[modified]="modified"
></cc-monaco-diff-editor>
</mat-card-content>
<mat-card-actions>
<div fxLayout="row" fxLayoutAlign="space-between center">
<button mat-button (click)="back()">
<mat-icon>keyboard_arrow_left</mat-icon>
BACK TO EDIT
</button>
<button mat-button color="primary">COMMIT</button>
</div>
</mat-card-actions>
</mat-card>
<mat-card *ngIf="!initialized">
Nothing to review
<mat-card-actions>
<button mat-button (click)="back()">
<mat-icon>keyboard_arrow_left</mat-icon>
BACK TO EDIT
</button></mat-card-actions
>
</mat-card>
</div>

View File

@ -0,0 +1,63 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { MatCheckboxChange } from '@angular/material';
import { Subscription } from 'rxjs';
import { MonacoFile, IDiffEditorOptions } from '../../monaco-editor/model';
import { DomainReviewService } from '../domain-review.service';
import { toMonacoFile } from '../utils';
@Component({
templateUrl: './domain-obj-review.component.html',
styleUrls: ['../editor-container.scss']
})
export class DomainObjReviewComponent implements OnInit, OnDestroy {
initialized = false;
original: MonacoFile;
modified: MonacoFile;
objectType: string;
options: IDiffEditorOptions = {
renderSideBySide: true
};
private ref: string;
private reviewModelSub: Subscription;
constructor(
private route: ActivatedRoute,
private router: Router,
private domainReviewService: DomainReviewService
) {}
ngOnInit() {
this.initialize();
}
ngOnDestroy() {
if (this.reviewModelSub) {
this.reviewModelSub.unsubscribe();
}
}
back() {
this.router.navigate(['domain', this.ref]);
}
renderSideBySide(e: MatCheckboxChange) {
this.options = { ...this.options, renderSideBySide: e.checked };
}
private initialize() {
this.route.params.subscribe(({ ref }) => (this.ref = ref));
this.reviewModelSub = this.domainReviewService.reviewModel.subscribe(model => {
if (!model) {
this.initialized = false;
return;
}
this.original = toMonacoFile(model.original.monacoContent);
this.modified = toMonacoFile(model.modified.monacoContent);
this.objectType = model.objectType;
this.initialized = true;
});
}
}

View File

@ -0,0 +1,31 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { RouterModule } from '@angular/router';
import {
MatCardModule,
MatButtonModule,
MatIconModule,
MatCheckboxModule
} from '@angular/material';
import { DomainObjReviewComponent } from './domain-obj-review.component';
import { MonacoEditorModule } from '../../monaco-editor/monaco-editor.module';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
declarations: [DomainObjReviewComponent],
imports: [
CommonModule,
FlexLayoutModule,
RouterModule,
MatCardModule,
MatButtonModule,
MatCheckboxModule,
MonacoEditorModule,
SharedModule,
MatIconModule
],
exports: [DomainObjReviewComponent]
})
export class DomainObjReviewModule {}

View File

@ -0,0 +1,2 @@
export * from './domain-obj-review.module';
export * from './domain-obj-review.component';

View File

@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { Subject, Observable, BehaviorSubject } from 'rxjs';
import { DomainModificationModel } from './domain-modification-model';
@Injectable()
export class DomainReviewService {
private reviewModel$: Subject<DomainModificationModel> = new BehaviorSubject(null);
get reviewModel(): Observable<DomainModificationModel> {
return this.reviewModel$;
}
addReviewModel(model: DomainModificationModel) {
this.reviewModel$.next(model);
}
}

View File

@ -4,6 +4,7 @@ import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '../app-auth-guard.service'; import { AppAuthGuardService } from '../app-auth-guard.service';
import { DomainInfoComponent } from './domain-info'; import { DomainInfoComponent } from './domain-info';
import { DomainObjModificationComponent } from './domain-obj-modification'; import { DomainObjModificationComponent } from './domain-obj-modification';
import { DomainObjReviewComponent } from './domain-obj-review';
@NgModule({ @NgModule({
imports: [ imports: [
@ -23,6 +24,14 @@ import { DomainObjModificationComponent } from './domain-obj-modification';
data: { data: {
roles: ['dmt:checkout'] roles: ['dmt:checkout']
} }
},
{
path: 'domain/:ref/review',
component: DomainObjReviewComponent,
canActivate: [AppAuthGuardService],
data: {
roles: ['dmt:checkout']
}
} }
]) ])
], ],

View File

@ -6,9 +6,17 @@ import { MetadataService } from './metadata.service';
import { DomainObjModificationModule } from './domain-obj-modification'; import { DomainObjModificationModule } from './domain-obj-modification';
import { DomainInfoModule } from './domain-info/domain-info.module'; import { DomainInfoModule } from './domain-info/domain-info.module';
import { DamselMetaModule } from '../damsel-meta/damsel-meta.module'; import { DamselMetaModule } from '../damsel-meta/damsel-meta.module';
import { DomainObjReviewModule } from './domain-obj-review';
import { DomainReviewService } from './domain-review.service';
@NgModule({ @NgModule({
imports: [DomainRoutingModule, DomainInfoModule, DomainObjModificationModule, DamselMetaModule], imports: [
providers: [DomainService, MetadataService] DomainRoutingModule,
DomainInfoModule,
DomainObjModificationModule,
DomainObjReviewModule,
DamselMetaModule
],
providers: [DomainService, MetadataService, DomainReviewService]
}) })
export class DomainModule {} export class DomainModule {}

View File

@ -0,0 +1,8 @@
.editor {
display: block;
height: calc(100vh - 200px);
}
.editor-container {
margin: 10px;
}

23
src/app/domain/utils.ts Normal file
View File

@ -0,0 +1,23 @@
import * as uuid from 'uuid/v4';
import { Reference, DomainObject } from '../gen-damsel/domain';
import { MonacoFile } from '../monaco-editor';
import { toJson } from '../shared/thrift-json-converter';
import { extract } from '../shared/thrift-utils';
export function parseRef(ref: string): Reference {
try {
return JSON.parse(ref);
} catch {
throw new Error('Malformed domain object ref');
}
}
export const toMonacoFile = (content: string): MonacoFile => ({
uri: `${uuid()}.json`,
language: 'json',
content
});
export const toMonacoContent = (domainObj: DomainObject): string =>
JSON.stringify(toJson(extract(domainObj)), null, 2);

View File

@ -1,4 +1,5 @@
export type IEditorOptions = monaco.editor.IEditorOptions; export type IEditorOptions = monaco.editor.IEditorOptions;
export type IDiffEditorOptions = monaco.editor.IDiffEditorOptions;
export type ITextModel = monaco.editor.ITextModel; export type ITextModel = monaco.editor.ITextModel;
export type CancellationToken = monaco.CancellationToken; export type CancellationToken = monaco.CancellationToken;
export type ProviderResult<T> = monaco.languages.ProviderResult<T>; export type ProviderResult<T> = monaco.languages.ProviderResult<T>;