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>
<button mat-button routerLink="/domain">
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
BACK TO DOMAIN
</button>
</div>
<div class="editor-container">
<div *ngIf="isLoading" fxLayout fxLayoutAlign="center stretch"><mat-spinner></mat-spinner></div>
<mat-card *ngIf="initialized">
<mat-card-header>
<mat-card-subtitle>{{ objectType }}</mat-card-subtitle>
<mat-card-title>Edit {{ model.objectType }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<cc-monaco-editor
[file]="file"
class="editor"
[file]="modifiedFile"
(fileChange)="fileChange($event)"
[options]="options"
[codeLensProviders]="codeLensProviders"
[completionProviders]="completionProviders"
></cc-monaco-editor>
</mat-card-content>
<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>
</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 { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import {
IEditorOptions,
MonacoFile,
CodeLensProvider,
CompletionProvider
} from '../../monaco-editor/model';
import { MonacoFile, CodeLensProvider, CompletionProvider } from '../../monaco-editor/model';
import { DomainObjModificationService } from './domain-obj-modification.service';
import { DomainObjCodeLensProvider } from './domain-obj-code-lens-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({
templateUrl: './domain-obj-modification.component.html',
styleUrls: ['domain-obj-modification.component.scss'],
styleUrls: ['../editor-container.scss'],
providers: [DomainObjModificationService]
})
export class DomainObjModificationComponent implements OnInit {
export class DomainObjModificationComponent implements OnInit, OnDestroy {
initialized = false;
isLoading: boolean;
file: MonacoFile;
options: IEditorOptions = {
readOnly: false
};
objectType: string;
valid = false;
codeLensProviders: CodeLensProvider[];
completionProviders: CompletionProvider[];
modifiedFile: MonacoFile;
private model: DomainModificationModel;
private initSub: Subscription;
constructor(
private router: Router,
private snackBar: MatSnackBar,
private domainObjModificationService: DomainObjModificationService
) {
this.domainObjModificationService.valueValid.subscribe(v => (this.valid = v));
}
private domainObjModService: DomainObjModificationService,
private domainReviewService: DomainReviewService
) {}
ngOnInit() {
this.initialize();
this.initSub = this.initialize();
this.codeLensProviders = [new DomainObjCodeLensProvider()];
this.completionProviders = [new DomainObjCompletionProvider()];
}
fileChange({ content }: MonacoFile) {
const meta = this.domainObjModificationService.applyValue(content);
console.log('BUILDED', meta);
ngOnDestroy() {
if (this.initSub) {
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.domainObjModificationService.initialize().subscribe(
({ file, objectType }) => {
return this.domainObjModService.init().subscribe(
model => {
this.isLoading = false;
this.file = file;
this.objectType = objectType;
this.model = model;
this.modifiedFile = toMonacoFile(model.modified.monacoContent);
this.initialized = true;
},
err => {
console.error(err);
this.isLoading = false;
this.snackBar
.open(`An error occurred while initializing: ${err}`, 'RETRY', {
duration: 10000
})
.onAction()
.subscribe(() => this.initialize());
this.snackBar.open(`An error occurred while initializing: ${err}`, 'OK');
}
);
}

View File

@ -1,39 +1,34 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable, combineLatest, Subject, BehaviorSubject } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';
import { Observable, combineLatest, Subject, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import cloneDeep from 'lodash-es/cloneDeep';
import { DomainObject, Reference } from '../../gen-damsel/domain';
import { MetadataService } from '../metadata.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 { MetaStruct, MetaUnion } from '../../damsel-meta/model';
import { ModificationPayload } from './modification-payload';
import { MetaStruct, MetaUnion, MetaPayload } from '../../damsel-meta/model';
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()
export class DomainObjModificationService {
private meta: MetaStruct | MetaUnion;
private errors$: Subject<string> = new Subject();
private valueValid$: Subject<boolean> = new BehaviorSubject(false);
get errors(): Observable<string> {
return this.errors$;
}
get valueValid(): Observable<boolean> {
return this.valueValid$;
}
constructor(
private route: ActivatedRoute,
private domainService: DomainService,
private metadataService: MetadataService,
private metaBuilder: MetaBuilder,
private metaApplicator: MetaApplicator
private metaApplicator: MetaApplicator,
private domainReviewService: DomainReviewService
) {
this.metaBuilder.errors.subscribe(e => {
this.errors$.next(e);
@ -42,29 +37,40 @@ export class DomainObjModificationService {
this.metaApplicator.errors.subscribe(e => console.log('Apply meta error:', e));
}
initialize(namespace = 'domain'): Observable<ModificationPayload> {
return this.route.params.pipe(
map(({ ref }) => this.parseParams(ref)),
switchMap(ref =>
combineLatest(
init(namespace = 'domain'): Observable<DomainModificationModel> {
return combineLatest(this.route.params, this.domainReviewService.reviewModel).pipe(
switchMap(([routeParams, model]) => {
if (model && JSON.stringify(model.ref) === routeParams.ref) {
return of(model);
}
const ref = parseRef(routeParams.ref);
return combineLatest(
this.metadataService.getDomainObjectType(ref),
this.domainService.getDomainObject(ref)
).pipe(
switchMap(([objectType, domainObj]) =>
this.build(ref, objectType, domainObj, namespace)
)
),
switchMap(([objectType, domainObj]) => this.buildMeta(objectType, domainObj, namespace))
);
})
);
}
applyValue(json: string): MetaStruct | MetaUnion | null {
if (!this.meta) {
throw new Error('Service is not initialized');
}
const result = this.metaApplicator.apply(this.meta, json);
this.valueValid$.next(result.valid);
return result.payload;
applyValue(meta: MetaStruct | MetaUnion, json: string): MetaPayload {
return this.metaApplicator.apply(meta, json);
}
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) {
throw new Error('Domain object type not found');
}
@ -72,32 +78,26 @@ export class DomainObjModificationService {
throw new Error('Domain object not found');
}
return this.metaBuilder.build(objectType, namespace).pipe(
tap(({ payload, valid }) => {
map(({ payload, valid }) => {
if (!valid) {
throw new Error('Build meta failed');
throw new Error('Build initial meta failed');
}
this.meta = payload;
}),
map(() => ({
file: this.toMonacoFile(domainObj),
const monacoContent = toMonacoContent(domainObj);
const applyResult = this.metaApplicator.apply(payload, monacoContent);
if (!applyResult.valid) {
throw new Error('Apply original value failed');
}
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 { DomainInfoComponent } from './domain-info';
import { DomainObjModificationComponent } from './domain-obj-modification';
import { DomainObjReviewComponent } from './domain-obj-review';
@NgModule({
imports: [
@ -23,6 +24,14 @@ import { DomainObjModificationComponent } from './domain-obj-modification';
data: {
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 { DomainInfoModule } from './domain-info/domain-info.module';
import { DamselMetaModule } from '../damsel-meta/damsel-meta.module';
import { DomainObjReviewModule } from './domain-obj-review';
import { DomainReviewService } from './domain-review.service';
@NgModule({
imports: [DomainRoutingModule, DomainInfoModule, DomainObjModificationModule, DamselMetaModule],
providers: [DomainService, MetadataService]
imports: [
DomainRoutingModule,
DomainInfoModule,
DomainObjModificationModule,
DomainObjReviewModule,
DamselMetaModule
],
providers: [DomainService, MetadataService, DomainReviewService]
})
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 IDiffEditorOptions = monaco.editor.IDiffEditorOptions;
export type ITextModel = monaco.editor.ITextModel;
export type CancellationToken = monaco.CancellationToken;
export type ProviderResult<T> = monaco.languages.ProviderResult<T>;