IMP-145: New domain config create and edit dialogs (#315)

This commit is contained in:
Rinat Arsaev 2024-01-25 11:20:48 +07:00 committed by GitHub
parent b5f95a3a24
commit e617dc82ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 736 additions and 838 deletions

9
package-lock.json generated
View File

@ -20,13 +20,12 @@
"@angular/platform-server": "17.0.8", "@angular/platform-server": "17.0.8",
"@angular/router": "17.0.8", "@angular/router": "17.0.8",
"@ngneat/input-mask": "6.0.0", "@ngneat/input-mask": "6.0.0",
"@s-libs/ng-core": "17.0.0",
"@vality/deanonimus-proto": "2.0.1-2a02d87.0", "@vality/deanonimus-proto": "2.0.1-2a02d87.0",
"@vality/domain-proto": "2.0.1-23211ff.0", "@vality/domain-proto": "2.0.1-23211ff.0",
"@vality/fistful-proto": "2.0.1-3b9a0a7.0", "@vality/fistful-proto": "2.0.1-3b9a0a7.0",
"@vality/machinegun-proto": "1.0.0", "@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-4383410.0", "@vality/magista-proto": "2.0.2-4383410.0",
"@vality/ng-core": "^17.1.1-pr-57-1a4e713.0", "@vality/ng-core": "^17.1.1-pr-57-8ca06c2.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0", "@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0", "@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0", "@vality/thrift-ts": "2.4.1-8ad5123.0",
@ -6523,9 +6522,9 @@
"integrity": "sha512-kAiKSTvof+jFuNkQKyAsc2s+Br2NXPWAyKuD0f7mQIk9HrP8uHsKJya5KxdOdng97JYe0MSUlx7seQxWmCgYfA==" "integrity": "sha512-kAiKSTvof+jFuNkQKyAsc2s+Br2NXPWAyKuD0f7mQIk9HrP8uHsKJya5KxdOdng97JYe0MSUlx7seQxWmCgYfA=="
}, },
"node_modules/@vality/ng-core": { "node_modules/@vality/ng-core": {
"version": "17.1.1-pr-57-1a4e713.0", "version": "17.1.1-pr-57-8ca06c2.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.1.1-pr-57-1a4e713.0.tgz", "resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.1.1-pr-57-8ca06c2.0.tgz",
"integrity": "sha512-Jar5VUIp5OISoCc0rhmcsk9EkVjzItlT2N/sMCFIKIKD51XDxn6NvYXOOwOiNAyu1LAd71mGPT8GvdWazR12cA==", "integrity": "sha512-io0j/knhIGgqmi8HxfuTZMGycVSO8qE9EYq5KN7lP7ectNeDrpvZdKcsJUg2kfTtWWaG9p8DSR5CHbe36tDxYA==",
"dependencies": { "dependencies": {
"@angular/material-date-fns-adapter": "^17.0.0", "@angular/material-date-fns-adapter": "^17.0.0",
"@ng-matero/extensions": "^17.0.0", "@ng-matero/extensions": "^17.0.0",

View File

@ -28,13 +28,12 @@
"@angular/platform-server": "17.0.8", "@angular/platform-server": "17.0.8",
"@angular/router": "17.0.8", "@angular/router": "17.0.8",
"@ngneat/input-mask": "6.0.0", "@ngneat/input-mask": "6.0.0",
"@s-libs/ng-core": "17.0.0",
"@vality/deanonimus-proto": "2.0.1-2a02d87.0", "@vality/deanonimus-proto": "2.0.1-2a02d87.0",
"@vality/domain-proto": "2.0.1-23211ff.0", "@vality/domain-proto": "2.0.1-23211ff.0",
"@vality/fistful-proto": "2.0.1-3b9a0a7.0", "@vality/fistful-proto": "2.0.1-3b9a0a7.0",
"@vality/machinegun-proto": "1.0.0", "@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-4383410.0", "@vality/magista-proto": "2.0.2-4383410.0",
"@vality/ng-core": "^17.1.1-pr-57-1a4e713.0", "@vality/ng-core": "^17.1.1-pr-57-8ca06c2.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0", "@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0", "@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0", "@vality/thrift-ts": "2.4.1-8ad5123.0",

View File

@ -4,19 +4,23 @@ import { Domain, DomainObject, Reference } from '@vality/domain-proto/domain';
import { Commit, Snapshot, Version } from '@vality/domain-proto/domain_config'; import { Commit, Snapshot, Version } from '@vality/domain-proto/domain_config';
import { NotifyLogService } from '@vality/ng-core'; import { NotifyLogService } from '@vality/ng-core';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import { BehaviorSubject, defer, Observable, of, ReplaySubject } from 'rxjs'; import { BehaviorSubject, defer, Observable, of, ReplaySubject, filter, combineLatest } from 'rxjs';
import { map, shareReplay, startWith, switchMap, take, tap } from 'rxjs/operators'; import { map, shareReplay, startWith, switchMap, take, tap } from 'rxjs/operators';
import { inProgressFrom, progressTo, getUnionKey } from '../../../../utils'; import { inProgressFrom, progressTo, getUnionKey } from '../../../../utils';
import { DomainSecretService } from '../../../shared/services'; import { DomainSecretService } from '../../../shared/services';
import { handleError } from '../../../shared/services/notification-error'; import { handleError } from '../../../shared/services/notification-error';
import { RepositoryService } from '../index'; import { RepositoryService } from '../repository.service';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class DomainStoreService { export class DomainStoreService {
version$ = defer(() => this.snapshot$).pipe(map((s) => s?.version)); version$ = combineLatest([defer(() => this.snapshot$), defer(() => this.progress$)]).pipe(
filter(([, p]) => !p),
map(([s]) => s.version),
take(1),
);
isLoading$ = inProgressFrom( isLoading$ = inProgressFrom(
() => this.progress$, () => this.progress$,
defer(() => this.snapshot$), defer(() => this.snapshot$),

View File

@ -2,12 +2,11 @@ import { Component, Input, OnChanges } from '@angular/core';
import { Validator } from '@angular/forms'; import { Validator } from '@angular/forms';
import { Claim } from '@vality/domain-proto/claim_management'; import { Claim } from '@vality/domain-proto/claim_management';
import { Party } from '@vality/domain-proto/domain'; import { Party } from '@vality/domain-proto/domain';
import { ComponentChanges } from '@vality/ng-core'; import { ComponentChanges, createControlProviders, FormControlSuperclass } from '@vality/ng-core';
import { from, combineLatest, ReplaySubject, defer } from 'rxjs'; import { from, combineLatest, ReplaySubject, defer } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services/domain-metadata-form-extensions'; import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services/domain-metadata-form-extensions';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { createPartyClaimMetadataFormExtensions } from './utils/create-party-claim-metadata-form-extensions'; import { createPartyClaimMetadataFormExtensions } from './utils/create-party-claim-metadata-form-extensions';
@ -17,7 +16,7 @@ import { createPartyClaimMetadataFormExtensions } from './utils/create-party-cla
providers: createControlProviders(() => ModificationFormComponent), providers: createControlProviders(() => ModificationFormComponent),
}) })
export class ModificationFormComponent export class ModificationFormComponent
extends ValidatedFormControlSuperclass<unknown> extends FormControlSuperclass<unknown>
implements Validator, OnChanges implements Validator, OnChanges
{ {
@Input() party: Party; @Input() party: Party;

View File

@ -3,14 +3,11 @@
title="Domain config" title="Domain config"
> >
<cc-page-layout-actions> <cc-page-layout-actions>
<button <button color="primary" mat-raised-button style="white-space: nowrap" (click)="create()">
color="primary" Create
mat-raised-button
routerLink="/domain/create"
style="white-space: nowrap"
>
Create object
</button> </button>
</cc-page-layout-actions> </cc-page-layout-actions>
<cc-domain-objects-table></cc-domain-objects-table> <cc-domain-objects-table
(selectedChange)="selectedTypes$.next($event)"
></cc-domain-objects-table>
</cc-page-layout> </cc-page-layout>

View File

@ -1,7 +1,13 @@
import { Component } from '@angular/core'; import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DialogService } from '@vality/ng-core';
import { BehaviorSubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config'; import { DomainStoreService } from '@cc/app/api/domain-config';
import { CreateDomainObjectDialogComponent } from '../../../shared/components/thrift-api-crud';
@Component({ @Component({
templateUrl: './domain-info.component.html', templateUrl: './domain-info.component.html',
styleUrls: ['./domain-info.component.scss'], styleUrls: ['./domain-info.component.scss'],
@ -9,6 +15,26 @@ import { DomainStoreService } from '@cc/app/api/domain-config';
export class DomainInfoComponent { export class DomainInfoComponent {
version$ = this.domainStoreService.version$; version$ = this.domainStoreService.version$;
progress$ = this.domainStoreService.isLoading$; progress$ = this.domainStoreService.isLoading$;
selectedTypes$ = new BehaviorSubject<string[]>([]);
constructor(private domainStoreService: DomainStoreService) {} constructor(
private domainStoreService: DomainStoreService,
private dialogService: DialogService,
private destroyRef: DestroyRef,
) {}
create() {
this.selectedTypes$
.pipe(first(), takeUntilDestroyed(this.destroyRef))
.subscribe((types) => {
this.dialogService.open(
CreateDomainObjectDialogComponent,
types?.length === 1
? {
objectType: types[0],
}
: undefined,
);
});
}
} }

View File

@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, DestroyRef, Output, EventEmitter } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { Sort } from '@angular/material/sort'; import { Sort } from '@angular/material/sort';
@ -11,6 +12,8 @@ import {
SelectFieldModule, SelectFieldModule,
TableModule, TableModule,
ActionsModule, ActionsModule,
DialogService,
getValueChanges,
} from '@vality/ng-core'; } from '@vality/ng-core';
import sortBy from 'lodash-es/sortBy'; import sortBy from 'lodash-es/sortBy';
import startCase from 'lodash-es/startCase'; import startCase from 'lodash-es/startCase';
@ -25,6 +28,7 @@ import {
DomainThriftViewerComponent, DomainThriftViewerComponent,
DomainObjectCardComponent, DomainObjectCardComponent,
DomainObjectService, DomainObjectService,
EditDomainObjectDialogComponent,
} from '../../../../shared/components/thrift-api-crud'; } from '../../../../shared/components/thrift-api-crud';
import { MetadataService } from '../../services/metadata.service'; import { MetadataService } from '../../services/metadata.service';
@ -49,6 +53,8 @@ interface DomainObjectData {
], ],
}) })
export class DomainObjectsTableComponent implements OnInit { export class DomainObjectsTableComponent implements OnInit {
@Output() selectedChange = new EventEmitter<string[]>();
typesControl = new FormControl<string[]>( typesControl = new FormControl<string[]>(
(this.qp.params.types as (keyof DomainObject)[]) || [], (this.qp.params.types as (keyof DomainObject)[]) || [],
); );
@ -105,7 +111,11 @@ export class DomainObjectsTableComponent implements OnInit {
{ {
label: 'Edit', label: 'Edit',
click: (d) => { click: (d) => {
void this.domainObjectService.edit(d.ref); this.dialogService
.open(EditDomainObjectDialogComponent, { domainObject: d.obj })
.afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe();
}, },
}, },
{ {
@ -138,12 +148,21 @@ export class DomainObjectsTableComponent implements OnInit {
private qp: QueryParamsService<{ types?: string[] }>, private qp: QueryParamsService<{ types?: string[] }>,
private sidenavInfoService: SidenavInfoService, private sidenavInfoService: SidenavInfoService,
private domainObjectService: DomainObjectService, private domainObjectService: DomainObjectService,
private destroyRef: DestroyRef,
private dialogService: DialogService,
) {} ) {}
ngOnInit() { ngOnInit() {
this.typesControl.valueChanges.subscribe((types) => { this.typesControl.valueChanges
void this.qp.patch({ types }); .pipe(takeUntilDestroyed(this.destroyRef))
}); .subscribe((types) => {
void this.qp.patch({ types });
});
getValueChanges(this.typesControl)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((types) => {
this.selectedChange.emit(types);
});
} }
update() { update() {

View File

@ -1,49 +0,0 @@
<cc-page-layout title="Create">
<mat-card>
<mat-card-content class="content">
<cc-thrift-editor
*ngIf="!review"
[extensions]="extensions$ | async"
[formControl]="control"
[metadata]="metadata$ | async"
class="editor"
namespace="domain"
type="DomainObject"
></cc-thrift-editor>
<cc-thrift-viewer
*ngIf="review"
[extensions]="viewerExtensions$ | async"
[metadata]="metadata$ | async"
[value]="control.value"
class="editor"
namespace="domain"
type="DomainObject"
></cc-thrift-viewer>
</mat-card-content>
</mat-card>
<v-actions>
<button *ngIf="!review" [disabled]="!!(progress$ | async)" mat-button routerLink="/domain">
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
Back to domain
</button>
<button
*ngIf="review"
[disabled]="!!(progress$ | async)"
mat-button
(click)="review = false"
>
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
Back to edit
</button>
<button
[disabled]="control.invalid || !!(progress$ | async)"
color="primary"
mat-button
(click)="review ? commit() : reviewChanges()"
>
{{ review ? 'Commit' : 'Review' }}
<mat-icon aria-label="Login">keyboard_arrow_right</mat-icon>
</button>
</v-actions>
</cc-page-layout>

View File

@ -1,64 +0,0 @@
import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, Validators } from '@angular/forms';
import { DomainObject } from '@vality/domain-proto/domain';
import { NotifyLogService } from '@vality/ng-core';
import { BehaviorSubject } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
import { DomainMetadataViewExtensionsService } from '@cc/app/shared/components/thrift-api-crud/domain/domain-thrift-viewer/services/domain-metadata-view-extensions';
import { progressTo, getUnionKey } from '../../../../utils';
import { DomainMetadataFormExtensionsService } from '../../../shared/services';
import { NotificationService } from '../../../shared/services/notification';
import { DomainNavigateService } from '../services/domain-navigate.service';
import { MetadataService } from '../services/metadata.service';
@Component({
templateUrl: './domain-obj-creation.component.html',
styleUrls: ['../editor-container.scss'],
})
export class DomainObjCreationComponent {
control = new FormControl<DomainObject>(null, Validators.required);
review = false;
metadata$ = this.metadataService.metadata;
extensions$ = this.domainMetadataFormExtensionsService.extensions$;
viewerExtensions$ = this.domainMetadataViewExtensionsService.extensions$;
progress$ = new BehaviorSubject(0);
constructor(
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService,
private domainMetadataViewExtensionsService: DomainMetadataViewExtensionsService,
private domainStoreService: DomainStoreService,
private notificationService: NotificationService,
private log: NotifyLogService,
private domainNavigateService: DomainNavigateService,
private metadataService: MetadataService,
private destroyRef: DestroyRef,
) {}
reviewChanges() {
this.review = true;
}
commit() {
this.domainStoreService
.commit({ ops: [{ insert: { object: this.control.value } }] })
.pipe(
withLatestFrom(
this.metadataService.getDomainFieldByFieldName(getUnionKey(this.control.value)),
),
progressTo(this.progress$),
takeUntilDestroyed(this.destroyRef),
)
.subscribe({
next: ([, field]) => {
this.notificationService.success('Successfully created');
void this.domainNavigateService.toType(String(field.type));
},
error: this.log.error,
});
}
}

View File

@ -1,37 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { ActionsModule } from '@vality/ng-core';
import { ThriftEditorModule } from '@cc/app/shared/components/thrift-editor';
import { PageLayoutModule } from '../../../shared';
import { ThriftViewerModule } from '../../../shared/components/thrift-viewer';
import { DomainObjCreationComponent } from './domain-obj-creation.component';
@NgModule({
declarations: [DomainObjCreationComponent],
imports: [
CommonModule,
RouterModule,
MatProgressSpinnerModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatDialogModule,
ReactiveFormsModule,
ThriftEditorModule,
ActionsModule,
ThriftViewerModule,
PageLayoutModule,
],
exports: [DomainObjCreationComponent],
})
export class DomainObjCreationModule {}

View File

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

View File

@ -1,35 +0,0 @@
<cc-page-layout title="Edit {{ type$ | async }}">
<div *ngIf="progress$ | async; else content" style="display: flex; justify-content: center">
<mat-spinner></mat-spinner>
</div>
<ng-template #content>
<mat-card>
<mat-card-content class="content">
<cc-thrift-editor
[defaultValue]="object$ | async"
[extensions]="extensions$ | async"
[formControl]="control"
[metadata]="metadata$ | async"
[type]="type$ | async"
class="editor"
namespace="domain"
></cc-thrift-editor>
</mat-card-content>
</mat-card>
<v-actions>
<button mat-button (click)="backToDomain()">
<mat-icon aria-label="Login">keyboard_arrow_left</mat-icon>
Back to domain
</button>
<button
[disabled]="control.invalid"
color="primary"
mat-button
(click)="reviewChanges()"
>
Review changes
<mat-icon aria-label="Login">keyboard_arrow_right</mat-icon>
</button>
</v-actions>
</ng-template>
</cc-page-layout>

View File

@ -1,65 +0,0 @@
import { Component, OnInit, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { from } from 'rxjs';
import { first } from 'rxjs/operators';
import { DomainMetadataFormExtensionsService } from '../../../shared/services';
import { DomainNavigateService } from '../services/domain-navigate.service';
import { DomainObjModificationService } from '../services/domain-obj-modification.service';
import { ModifiedDomainObjectService } from '../services/modified-domain-object.service';
@Component({
templateUrl: './domain-obj-modification.component.html',
styleUrls: ['../editor-container.scss'],
providers: [DomainObjModificationService],
})
export class DomainObjModificationComponent implements OnInit {
control = new FormControl();
progress$ = this.domainObjModService.progress$;
metadata$ = from(import('@vality/domain-proto/metadata.json').then((m) => m.default));
object$ = this.domainObjModService.object$;
type$ = this.domainObjModService.type$;
extensions$ = this.domainMetadataFormExtensionsService.extensions$;
constructor(
private router: Router,
private route: ActivatedRoute,
private domainObjModService: DomainObjModificationService,
private modifiedDomainObjectService: ModifiedDomainObjectService,
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService,
private domainNavigateService: DomainNavigateService,
private destroyRef: DestroyRef,
) {}
ngOnInit() {
this.domainObjModService.object$
.pipe(first(), takeUntilDestroyed(this.destroyRef))
.subscribe((object) => {
if (
this.modifiedDomainObjectService.domainObject &&
this.route.snapshot.queryParams.ref === this.modifiedDomainObjectService.ref
) {
this.control.setValue(this.modifiedDomainObjectService.domainObject);
} else {
this.control.setValue(object);
}
});
}
reviewChanges() {
this.modifiedDomainObjectService.update(
this.control.value,
this.route.snapshot.queryParams.ref,
);
void this.router.navigate(['domain', 'review'], {
queryParams: { ref: this.route.snapshot.queryParams.ref },
});
}
backToDomain() {
this.type$.pipe(first()).subscribe((type) => this.domainNavigateService.toType(type));
}
}

View File

@ -1,36 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { ActionsModule } from '@vality/ng-core';
import { ThriftEditorModule } from '@cc/app/shared/components/thrift-editor';
import { PageLayoutModule } from '../../../shared';
import { DomainObjModificationComponent } from './domain-obj-modification.component';
@NgModule({
declarations: [DomainObjModificationComponent],
imports: [
CommonModule,
RouterModule,
MatProgressSpinnerModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatDialogModule,
ReactiveFormsModule,
ThriftEditorModule,
ActionsModule,
PageLayoutModule,
],
exports: [DomainObjModificationComponent],
})
export class DomainObjModificationModule {}

View File

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

View File

@ -1,31 +0,0 @@
<cc-page-layout title="Review changes of {{ type$ | async }}">
<div *ngIf="progress$ | async; else content" style="display: flex; justify-content: center">
<mat-spinner></mat-spinner>
</div>
<ng-template #content>
<mat-card>
<mat-card-content class="content">
<cc-thrift-viewer
[compared]="modifiedObject"
[value]="object$ | async"
class="editor"
kind="diff"
></cc-thrift-viewer>
</mat-card-content>
</mat-card>
<v-actions>
<button [disabled]="!!(progress$ | async)" mat-button (click)="back()">
<mat-icon>keyboard_arrow_left</mat-icon>
Back to edit
</button>
<button
[disabled]="!!(progress$ | async)"
color="primary"
mat-button
(click)="commit()"
>
Commit
</button>
</v-actions>
</ng-template>
</cc-page-layout>

View File

@ -1,83 +0,0 @@
import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router, ActivatedRoute } from '@angular/router';
import { switchMap } from 'rxjs';
import { first, withLatestFrom } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
import { DomainSecretService } from '@cc/app/shared/services/domain-secret-service';
import { NotificationErrorService } from '@cc/app/shared/services/notification-error';
import { getUnionKey } from '../../../../utils';
import { NotificationService } from '../../../shared/services/notification';
import { DomainNavigateService } from '../services/domain-navigate.service';
import { DomainObjModificationService } from '../services/domain-obj-modification.service';
import { ModifiedDomainObjectService } from '../services/modified-domain-object.service';
@Component({
templateUrl: './domain-obj-review.component.html',
styleUrls: ['../editor-container.scss'],
providers: [DomainObjModificationService],
})
export class DomainObjReviewComponent {
progress$ = this.domainObjModService.progress$;
object$ = this.domainObjModService.object$;
type$ = this.domainObjModService.type$;
modifiedObject = this.modifiedDomainObjectService.domainObject;
constructor(
private router: Router,
private route: ActivatedRoute,
private domainObjModService: DomainObjModificationService,
private modifiedDomainObjectService: ModifiedDomainObjectService,
private domainStoreService: DomainStoreService,
private notificationService: NotificationService,
private notificationErrorService: NotificationErrorService,
private domainNavigateService: DomainNavigateService,
private domainSecretService: DomainSecretService,
private destroyRef: DestroyRef,
) {
if (!modifiedDomainObjectService.domainObject) {
this.back();
}
}
commit() {
this.domainObjModService.fullObject$
.pipe(
first(),
// eslint-disable-next-line @typescript-eslint/naming-convention
switchMap((old_object) =>
this.domainStoreService.commit({
ops: [
{
update: {
old_object,
new_object: this.domainSecretService.restoreDomain(old_object, {
[getUnionKey(old_object)]: this.modifiedObject,
}),
},
},
],
}),
),
withLatestFrom(this.type$),
takeUntilDestroyed(this.destroyRef),
)
.subscribe({
next: ([, type]) => {
this.notificationService.success('Successfully changed');
void this.domainNavigateService.toType(type);
},
error: (err) => {
this.notificationErrorService.error(err);
},
});
}
back() {
void this.router.navigate(['domain', 'edit'], {
queryParams: { ref: this.route.snapshot.queryParams.ref },
});
}
}

View File

@ -1,37 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { RouterModule } from '@angular/router';
import { ActionsModule } from '@vality/ng-core';
import { PageLayoutModule } from '../../../shared';
import { ThriftEditorModule } from '../../../shared/components/thrift-editor';
import { ThriftViewerModule } from '../../../shared/components/thrift-viewer';
import { DomainObjReviewComponent } from './domain-obj-review.component';
@NgModule({
declarations: [DomainObjReviewComponent],
imports: [
CommonModule,
RouterModule,
MatCardModule,
MatButtonModule,
MatCheckboxModule,
MatIconModule,
ThriftEditorModule,
MatProgressSpinnerModule,
ActionsModule,
ReactiveFormsModule,
ThriftViewerModule,
PageLayoutModule,
],
exports: [DomainObjReviewComponent],
})
export class DomainObjReviewModule {}

View File

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

View File

@ -4,9 +4,6 @@ import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '@cc/app/shared/services'; import { AppAuthGuardService } from '@cc/app/shared/services';
import { DomainInfoComponent } from './domain-info'; import { DomainInfoComponent } from './domain-info';
import { DomainObjCreationComponent } from './domain-obj-creation';
import { DomainObjModificationComponent } from './domain-obj-modification';
import { DomainObjReviewComponent } from './domain-obj-review';
import { ROUTING_CONFIG } from './routing-config'; import { ROUTING_CONFIG } from './routing-config';
@NgModule({ @NgModule({
@ -21,18 +18,6 @@ import { ROUTING_CONFIG } from './routing-config';
path: '', path: '',
component: DomainInfoComponent, component: DomainInfoComponent,
}, },
{
path: 'create',
component: DomainObjCreationComponent,
},
{
path: 'edit',
component: DomainObjModificationComponent,
},
{
path: 'review',
component: DomainObjReviewComponent,
},
], ],
}, },
]), ]),

View File

@ -1,19 +1,11 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { DomainInfoModule } from './domain-info'; import { DomainInfoModule } from './domain-info';
import { DomainObjModificationModule } from './domain-obj-modification';
import { DomainObjReviewModule } from './domain-obj-review';
import { DomainRoutingModule } from './domain-routing.module'; import { DomainRoutingModule } from './domain-routing.module';
import { MetadataService } from './services/metadata.service';
import { ModifiedDomainObjectService } from './services/modified-domain-object.service'; import { ModifiedDomainObjectService } from './services/modified-domain-object.service';
@NgModule({ @NgModule({
imports: [ imports: [DomainRoutingModule, DomainInfoModule],
DomainRoutingModule, providers: [ModifiedDomainObjectService],
DomainInfoModule,
DomainObjModificationModule,
DomainObjReviewModule,
],
providers: [MetadataService, ModifiedDomainObjectService],
}) })
export class DomainModule {} export class DomainModule {}

View File

@ -1,19 +1,16 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ThriftAstMetadata } from '@vality/domain-proto'; import { ThriftAstMetadata } from '@vality/domain-proto';
import { Reference } from '@vality/domain-proto/domain'; import { Reference } from '@vality/domain-proto/domain';
import { getImportValue } from '@vality/ng-core';
import { Field } from '@vality/thrift-ts'; import { Field } from '@vality/thrift-ts';
import { from, Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators'; import { map, shareReplay, withLatestFrom } from 'rxjs/operators';
@Injectable() @Injectable({ providedIn: 'root' })
export class MetadataService { export class MetadataService {
private metadata$: Observable<ThriftAstMetadata[]> = from( private metadata$ = getImportValue<ThriftAstMetadata[]>(
import('@vality/domain-proto/metadata.json').then((m) => m.default), import('@vality/domain-proto/metadata.json'),
).pipe(shareReplay(1)) as Observable<ThriftAstMetadata[]>; ).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
get metadata() {
return this.metadata$;
}
getDomainObjectType(ref: Reference): Observable<string | null> { getDomainObjectType(ref: Reference): Observable<string | null> {
if (!ref) { if (!ref) {
@ -32,12 +29,31 @@ export class MetadataService {
); );
} }
getDomainFieldByFieldName(fieldName: string): Observable<Field> { getDomainFieldByName(fieldName: string): Observable<Field> {
return this.getDomainFields().pipe( return this.getDomainFields().pipe(
map((fields) => fields.find((f) => f.name === fieldName)), map((fields) => fields.find((f) => f.name === fieldName)),
); );
} }
getDomainFieldByType(fieldType: string): Observable<Field> {
return this.getDomainFields().pipe(
map((fields) => fields.find((f) => f.type === fieldType)),
);
}
getDomainObjectDataFieldByName(fieldName: string): Observable<Field> {
return this.getDomainFields().pipe(
map((fields) => fields.find((f) => f.name === fieldName)),
withLatestFrom(this.metadata$),
map(
([field, metadata]) =>
metadata
.find(({ name }) => name === 'domain')
.ast.struct[String(field.type)]?.find((f) => f.name === 'data'),
),
);
}
getDomainFields(): Observable<Field[]> { getDomainFields(): Observable<Field[]> {
return this.metadata$.pipe( return this.metadata$.pipe(
map((m) => m.find(({ name }) => name === 'domain').ast.union.DomainObject), map((m) => m.find(({ name }) => name === 'domain').ast.union.DomainObject),

View File

@ -1,7 +1,7 @@
import { Component, OnInit, DestroyRef } from '@angular/core'; import { Component, OnInit, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Validators, FormControl } from '@angular/forms'; import { Validators, FormControl } from '@angular/forms';
import { DialogResponseStatus, DialogSuperclass } from '@vality/ng-core'; import { DialogResponseStatus, DialogSuperclass, getValue } from '@vality/ng-core';
import { import {
RepairInvoicesRequest, RepairInvoicesRequest,
RepairWithdrawalsRequest, RepairWithdrawalsRequest,
@ -9,11 +9,12 @@ import {
} from '@vality/repairer-proto/repairer'; } from '@vality/repairer-proto/repairer';
import isNil from 'lodash-es/isNil'; import isNil from 'lodash-es/isNil';
import { BehaviorSubject, from } from 'rxjs'; import { BehaviorSubject, from } from 'rxjs';
import { map } from 'rxjs/operators';
import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services'; import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services';
import { NotificationErrorService } from '@cc/app/shared/services/notification-error'; import { NotificationErrorService } from '@cc/app/shared/services/notification-error';
import { progressTo, getFormValueChanges } from '../../../../../utils'; import { progressTo } from '../../../../../utils';
import { RepairManagementService } from '../../../../api/repairer'; import { RepairManagementService } from '../../../../api/repairer';
import { NotificationService } from '../../../../shared/services/notification'; import { NotificationService } from '../../../../shared/services/notification';
@ -66,8 +67,11 @@ export class RepairByScenarioDialogComponent
} }
ngOnInit() { ngOnInit() {
getFormValueChanges(this.nsControl) this.nsControl.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(
map(() => getValue(this.nsControl)),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(() => { .subscribe(() => {
this.form.setValue( this.form.setValue(
this.dialogData.machines.map(({ id }) => ({ id, scenario: {} })), this.dialogData.machines.map(({ id }) => ({ id, scenario: {} })),

View File

@ -1,5 +1,7 @@
<cc-page-layout title="Terminals"> <cc-page-layout title="Terminals">
<cc-page-layout-actions></cc-page-layout-actions> <cc-page-layout-actions>
<button color="primary" mat-raised-button (click)="create()">Create</button>
</cc-page-layout-actions>
<v-table <v-table
[(sort)]="sort" [(sort)]="sort"
[columns]="columns" [columns]="columns"
@ -9,9 +11,5 @@
sortOnFront sortOnFront
standaloneFilter standaloneFilter
(update)="update()" (update)="update()"
> ></v-table>
<v-table-actions>
<button color="primary" mat-raised-button (click)="create()">Create</button>
</v-table-actions>
</v-table>
</cc-page-layout> </cc-page-layout>

View File

@ -1,9 +1,8 @@
import { Component, DestroyRef } from '@angular/core'; import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Sort } from '@angular/material/sort'; import { Sort } from '@angular/material/sort';
import { Router } from '@angular/router';
import { TerminalObject } from '@vality/domain-proto/domain'; import { TerminalObject } from '@vality/domain-proto/domain';
import { Column } from '@vality/ng-core'; import { Column, DialogService } from '@vality/ng-core';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { map, take } from 'rxjs/operators'; import { map, take } from 'rxjs/operators';
@ -11,7 +10,10 @@ import { DomainStoreService } from '../../api/domain-config';
import { createPredicateColumn } from '../../shared'; import { createPredicateColumn } from '../../shared';
import { SidenavInfoService } from '../../shared/components/sidenav-info'; import { SidenavInfoService } from '../../shared/components/sidenav-info';
import { TerminalDelegatesCardComponent } from '../../shared/components/terminal-delegates-card/terminal-delegates-card.component'; import { TerminalDelegatesCardComponent } from '../../shared/components/terminal-delegates-card/terminal-delegates-card.component';
import { DomainObjectCardComponent } from '../../shared/components/thrift-api-crud'; import {
DomainObjectCardComponent,
CreateDomainObjectDialogComponent,
} from '../../shared/components/thrift-api-crud';
import { getTerminalShopWalletDelegates } from './utils/get-terminal-shop-wallet-delegates'; import { getTerminalShopWalletDelegates } from './utils/get-terminal-shop-wallet-delegates';
@ -71,9 +73,9 @@ export class TerminalsComponent {
constructor( constructor(
private domainStoreService: DomainStoreService, private domainStoreService: DomainStoreService,
private router: Router,
private sidenavInfoService: SidenavInfoService, private sidenavInfoService: SidenavInfoService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private dialogService: DialogService,
) {} ) {}
update() { update() {
@ -81,7 +83,9 @@ export class TerminalsComponent {
} }
create() { create() {
void this.router.navigate(['/domain/create']); this.dialogService.open(CreateDomainObjectDialogComponent, {
objectType: 'TerminalObject',
});
} }
private getProvider(terminalObj: TerminalObject) { private getProvider(terminalObj: TerminalObject) {

View File

@ -7,12 +7,11 @@ import {
FormControl, FormControl,
AbstractControl, AbstractControl,
} from '@angular/forms'; } from '@angular/forms';
import { FormComponentSuperclass } from '@s-libs/ng-core'; import { FormComponentSuperclass, createControlProviders, getErrorsTree } from '@vality/ng-core';
import { MapType, SetType, ListType } from '@vality/thrift-ts'; import { MapType, SetType, ListType } from '@vality/thrift-ts';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
import { MetadataFormExtension } from '@cc/app/shared/components/metadata-form'; import { MetadataFormExtension } from '@cc/app/shared/components/metadata-form';
import { createControlProviders, getErrorsTree } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
@ -65,20 +64,22 @@ export class ComplexFormComponent<V, K = never>
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
const values = this.valueControls.value; const values = this.valueControls.value;
if (!this.data.isRequired && !values.length) {
this.emitOutgoingValue(null);
return;
}
switch (this.data.type.name) { switch (this.data.type.name) {
case 'list': case 'list':
this.emitOutgoingValue(values.length ? values : null); this.emitOutgoingValue(values);
break; return;
case 'map': { case 'map': {
const keys = this.keyControls.value; const keys = this.keyControls.value;
this.emitOutgoingValue( this.emitOutgoingValue(new Map(values.map((v, idx) => [keys[idx], v])));
keys.length ? new Map(values.map((v, idx) => [keys[idx], v])) : null, return;
);
break;
} }
case 'set': case 'set':
this.emitOutgoingValue(values.length ? new Set(values) : null); this.emitOutgoingValue(new Set(values));
break; return;
} }
}); });
} }

View File

@ -1,6 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { createControlProviders, FormControlSuperclass } from '@vality/ng-core';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
@ -9,6 +8,6 @@ import { MetadataFormData } from '../../types/metadata-form-data';
templateUrl: './enum-field.component.html', templateUrl: './enum-field.component.html',
providers: createControlProviders(() => EnumFieldComponent), providers: createControlProviders(() => EnumFieldComponent),
}) })
export class EnumFieldComponent<T> extends ValidatedFormControlSuperclass<T> { export class EnumFieldComponent<T> extends FormControlSuperclass<T> {
@Input() data: MetadataFormData<string, 'enum'>; @Input() data: MetadataFormData<string, 'enum'>;
} }

View File

@ -1,8 +1,7 @@
import { Component, Input, OnChanges, OnInit, DestroyRef } from '@angular/core'; import { Component, Input, OnChanges, OnInit, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Validator, ValidationErrors, FormControl, Validators } from '@angular/forms'; import { Validator, ValidationErrors, FormControl, Validators } from '@angular/forms';
import { FormComponentSuperclass } from '@s-libs/ng-core'; import { FormComponentSuperclass, ComponentChanges, createControlProviders } from '@vality/ng-core';
import { ComponentChanges, createControlProviders } from '@vality/ng-core';
import { ThriftType } from '@vality/thrift-ts'; import { ThriftType } from '@vality/thrift-ts';
import { defer, switchMap, ReplaySubject, Observable, combineLatest } from 'rxjs'; import { defer, switchMap, ReplaySubject, Observable, combineLatest } from 'rxjs';
import { shareReplay, first, map, pluck } from 'rxjs/operators'; import { shareReplay, first, map, pluck } from 'rxjs/operators';

View File

@ -1,14 +1,24 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges, DestroyRef } from '@angular/core'; import { Component, Input, OnChanges, OnInit, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ValidationErrors, Validators, FormBuilder, FormGroup } from '@angular/forms'; import {
ValidationErrors,
Validators,
FormBuilder,
FormGroup,
AbstractControl,
} from '@angular/forms';
import {
createControlProviders,
ComponentChanges,
FormComponentSuperclass,
getValueChanges,
} from '@vality/ng-core';
import isNil from 'lodash-es/isNil'; import isNil from 'lodash-es/isNil';
import omitBy from 'lodash-es/omitBy'; import omitBy from 'lodash-es/omitBy';
import { merge } from 'rxjs'; import { combineLatest } from 'rxjs';
import { delay } from 'rxjs/operators'; import { map, distinctUntilChanged } from 'rxjs/operators';
import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils'; import { MetadataFormData, isRequiredField } from '../../types/metadata-form-data';
import { MetadataFormData } from '../../types/metadata-form-data';
import { MetadataFormExtension } from '../../types/metadata-form-extension'; import { MetadataFormExtension } from '../../types/metadata-form-extension';
@Component({ @Component({
@ -17,7 +27,7 @@ import { MetadataFormExtension } from '../../types/metadata-form-extension';
providers: createControlProviders(() => StructFormComponent), providers: createControlProviders(() => StructFormComponent),
}) })
export class StructFormComponent<T extends { [N in string]: unknown }> export class StructFormComponent<T extends { [N in string]: unknown }>
extends ValidatedControlSuperclass<T> extends FormComponentSuperclass<T>
implements OnChanges, OnInit implements OnChanges, OnInit
{ {
@Input() data: MetadataFormData<string, 'struct'>; @Input() data: MetadataFormData<string, 'struct'>;
@ -42,39 +52,38 @@ export class StructFormComponent<T extends { [N in string]: unknown }>
} }
ngOnInit() { ngOnInit() {
merge(this.control.valueChanges, this.labelControl.valueChanges) combineLatest([getValueChanges(this.control), getValueChanges(this.labelControl)])
.pipe(delay(0), takeUntilDestroyed(this.destroyRef)) .pipe(
.subscribe(() => { map(([value, labelValue]) =>
this.emitOutgoingValue( value && labelValue ? (omitBy(value, isNil) as T) : null,
this.control.value && this.labelControl.value ),
? (omitBy(this.control.value, isNil) as T) distinctUntilChanged(),
: null, takeUntilDestroyed(this.destroyRef),
); )
.subscribe((value) => {
this.emitOutgoingValue(value);
}); });
return super.ngOnInit(); return super.ngOnInit();
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: ComponentChanges<StructFormComponent<T>>) {
const newControlsNames = new Set(this.data.ast.map(({ name }) => name)); if (changes.data) {
Object.keys(this.control.controls).forEach((name) => { const newControlsNames = new Set(this.data.ast.map(({ name }) => name));
if (newControlsNames.has(name)) { Object.keys(this.control.controls).forEach((name) => {
newControlsNames.delete(name);
} else {
this.control.removeControl(name as never); this.control.removeControl(name as never);
} });
}); newControlsNames.forEach((name) =>
newControlsNames.forEach((name) => this.control.addControl(
this.control.addControl( name as never,
name as never, this.fb.control(null, {
this.fb.control(null, { validators: isRequiredField(this.data.ast.find((f) => f.name === name))
validators:
this.data.ast.find((f) => f.name === name)?.option === 'required'
? [Validators.required] ? [Validators.required]
: [], : [],
}) as never, }) as never,
), ),
); );
this.setLabelControl(); this.setLabelControl();
}
super.ngOnChanges(changes); super.ngOnChanges(changes);
} }
@ -83,8 +92,8 @@ export class StructFormComponent<T extends { [N in string]: unknown }>
this.setLabelControl(!!(value && Object.keys(value).length)); this.setLabelControl(!!(value && Object.keys(value).length));
} }
validate(): ValidationErrors | null { validate(control: AbstractControl): ValidationErrors | null {
return this.labelControl.value ? super.validate() : null; return this.labelControl.value ? super.validate(control) : null;
} }
private setLabelControl(value: boolean = false) { private setLabelControl(value: boolean = false) {

View File

@ -1,6 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { createControlProviders, FormControlSuperclass } from '@vality/ng-core';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
import { MetadataFormExtension } from '../../types/metadata-form-extension'; import { MetadataFormExtension } from '../../types/metadata-form-extension';
@ -10,7 +9,7 @@ import { MetadataFormExtension } from '../../types/metadata-form-extension';
templateUrl: './typedef-form.component.html', templateUrl: './typedef-form.component.html',
providers: createControlProviders(() => TypedefFormComponent), providers: createControlProviders(() => TypedefFormComponent),
}) })
export class TypedefFormComponent<T> extends ValidatedFormControlSuperclass<T> { export class TypedefFormComponent<T> extends FormControlSuperclass<T> {
@Input() data: MetadataFormData<string, 'typedef'>; @Input() data: MetadataFormData<string, 'typedef'>;
@Input() extensions: MetadataFormExtension[]; @Input() extensions: MetadataFormExtension[];
} }

View File

@ -6,8 +6,8 @@
[required]="data.isRequired" [required]="data.isRequired"
(ngModelChange)="cleanInternal()" (ngModelChange)="cleanInternal()"
> >
<mat-option *ngFor="let field of data.ast" [value]="field">{{ <mat-option *ngFor="let option of options$ | async" [value]="option.value">{{
field.type | fieldLabel: field option.label
}}</mat-option> }}</mat-option>
</mat-select> </mat-select>
<button <button

View File

@ -1,16 +1,19 @@
import { Component, Input, OnInit, DestroyRef } from '@angular/core'; import { Component, Input, OnInit, DestroyRef, OnChanges } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ValidationErrors, Validator, FormControl } from '@angular/forms'; import { ValidationErrors, Validator, FormControl } from '@angular/forms';
import { FormComponentSuperclass } from '@s-libs/ng-core'; import {
FormComponentSuperclass,
createControlProviders,
getErrorsTree,
ComponentChanges,
} from '@vality/ng-core';
import { Field } from '@vality/thrift-ts'; import { Field } from '@vality/thrift-ts';
import { merge } from 'rxjs'; import { merge, ReplaySubject, defer } from 'rxjs';
import { delay, distinctUntilChanged, map } from 'rxjs/operators'; import { delay, distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
import { createControlProviders, getErrorsTree } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
import { MetadataFormExtension } from '../../types/metadata-form-extension'; import { MetadataFormExtension } from '../../types/metadata-form-extension';
import { getDefaultValue } from '../../utils/get-default-value'; import { getFieldLabel, getDefaultValue } from '../../utils';
@Component({ @Component({
selector: 'cc-union-field', selector: 'cc-union-field',
@ -19,7 +22,7 @@ import { getDefaultValue } from '../../utils/get-default-value';
}) })
export class UnionFieldComponent<T extends { [N in string]: unknown }> export class UnionFieldComponent<T extends { [N in string]: unknown }>
extends FormComponentSuperclass<T> extends FormComponentSuperclass<T>
implements OnInit, Validator implements OnInit, Validator, OnChanges
{ {
@Input() data: MetadataFormData<string, 'union'>; @Input() data: MetadataFormData<string, 'union'>;
@Input() extensions: MetadataFormExtension[]; @Input() extensions: MetadataFormExtension[];
@ -27,6 +30,17 @@ export class UnionFieldComponent<T extends { [N in string]: unknown }>
fieldControl = new FormControl() as FormControl<Field>; fieldControl = new FormControl() as FormControl<Field>;
internalControl = new FormControl() as FormControl<T[keyof T]>; internalControl = new FormControl() as FormControl<T[keyof T]>;
protected options$ = defer(() => this.data$).pipe(
map((data) =>
data.ast
.map((field) => ({ label: getFieldLabel(field.type, field), value: field }))
.sort((a, b) => a.label.localeCompare(b.label)),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
private data$ = new ReplaySubject<MetadataFormData<string, 'union'>>(1);
constructor(private destroyRef: DestroyRef) { constructor(private destroyRef: DestroyRef) {
super(); super();
} }
@ -50,6 +64,13 @@ export class UnionFieldComponent<T extends { [N in string]: unknown }>
}); });
} }
ngOnChanges(changes: ComponentChanges<UnionFieldComponent<T>>) {
super.ngOnChanges(changes);
if (changes.data) {
this.data$.next(this.data);
}
}
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.fieldControl.errors || getErrorsTree(this.internalControl); return this.fieldControl.errors || getErrorsTree(this.internalControl);
} }
@ -70,13 +91,11 @@ export class UnionFieldComponent<T extends { [N in string]: unknown }>
cleanInternal(emitEvent = false) { cleanInternal(emitEvent = false) {
this.internalControl.reset( this.internalControl.reset(
this.fieldControl.value getDefaultValue(
? (getDefaultValue( this.data.metadata,
this.data.metadata, this.data.namespace,
this.data.namespace, this.fieldControl.value?.type,
this.fieldControl.value.type, ) as T[keyof T],
) as T[keyof T])
: null,
{ emitEvent }, { emitEvent },
); );
} }

View File

@ -1,11 +1,10 @@
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { Validator } from '@angular/forms'; import { Validator } from '@angular/forms';
import { ThriftAstMetadata } from '@vality/domain-proto'; import { ThriftAstMetadata } from '@vality/domain-proto';
import { createControlProviders, FormControlSuperclass } from '@vality/ng-core';
import { Field, ValueType } from '@vality/thrift-ts'; import { Field, ValueType } from '@vality/thrift-ts';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from './types/metadata-form-data'; import { MetadataFormData } from './types/metadata-form-data';
import { import {
MetadataFormExtension, MetadataFormExtension,
@ -19,7 +18,7 @@ import {
providers: createControlProviders(() => MetadataFormComponent), providers: createControlProviders(() => MetadataFormComponent),
}) })
export class MetadataFormComponent<T> export class MetadataFormComponent<T>
extends ValidatedFormControlSuperclass<T> extends FormControlSuperclass<T>
implements OnChanges, Validator implements OnChanges, Validator
{ {
@Input() metadata: ThriftAstMetadata[]; @Input() metadata: ThriftAstMetadata[];

View File

@ -1,14 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { ValueType, Field } from '@vality/thrift-ts'; import { ValueType, Field } from '@vality/thrift-ts';
import startCase from 'lodash-es/startCase';
import { getValueTypeTitle } from '@cc/app/shared'; import { getFieldLabel } from '../utils';
@Pipe({ @Pipe({
name: 'fieldLabel', name: 'fieldLabel',
}) })
export class FieldLabelPipe implements PipeTransform { export class FieldLabelPipe implements PipeTransform {
transform(type: ValueType, field?: Field): string { transform(type: ValueType, field?: Field): string {
return type ? startCase((field ? field.name : getValueTypeTitle(type)).toLowerCase()) : ''; return getFieldLabel(type, field);
} }
} }

View File

@ -45,6 +45,10 @@ export function isTypeWithAliases(
return Boolean(getByType(data, type, namespace)); return Boolean(getByType(data, type, namespace));
} }
export function isRequiredField(field: Field): boolean {
return field?.option === 'required'; // optional even if not explicitly stated
}
export class MetadataFormData< export class MetadataFormData<
T extends ValueType = ValueType, T extends ValueType = ValueType,
S extends StructureType = StructureType, S extends StructureType = StructureType,
@ -86,7 +90,7 @@ export class MetadataFormData<
} }
get isRequired() { get isRequired() {
return this.field?.option === 'required' || this.trueParent?.objectType === 'union'; return isRequiredField(this.field) || this.trueParent?.objectType === 'union';
} }
constructor( constructor(

View File

@ -5,6 +5,9 @@ import { TypeDefs } from '@vality/thrift-ts/src/thrift-parser';
import { MetadataFormData, TypeGroup } from '../types/metadata-form-data'; import { MetadataFormData, TypeGroup } from '../types/metadata-form-data';
export function getDefaultValue(metadata: ThriftAstMetadata[], namespace: string, type: ValueType) { export function getDefaultValue(metadata: ThriftAstMetadata[], namespace: string, type: ValueType) {
if (!type) {
return null;
}
let data: MetadataFormData; let data: MetadataFormData;
do { do {
data = new MetadataFormData(metadata, namespace, type); data = new MetadataFormData(metadata, namespace, type);

View File

@ -0,0 +1,8 @@
import { Field, ValueType } from '@vality/thrift-ts';
import startCase from 'lodash-es/startCase';
import { getValueTypeTitle } from '../../../pipes';
export function getFieldLabel(type: ValueType, field?: Field) {
return type ? startCase((field ? field.name : getValueTypeTitle(type)).toLowerCase()) : '';
}

View File

@ -0,0 +1,2 @@
export * from './get-field-label';
export * from './get-default-value';

View File

@ -1,7 +1,7 @@
import { Component, Input, OnInit, booleanAttribute } from '@angular/core'; import { Component, Input, OnInit, booleanAttribute } from '@angular/core';
import { PayoutTool } from '@vality/domain-proto/domain'; import { PayoutTool } from '@vality/domain-proto/domain';
import { PartyID, ShopID } from '@vality/domain-proto/payment_processing'; import { PartyID, ShopID } from '@vality/domain-proto/payment_processing';
import { createControlProviders, FormControlSuperclass, Option } from '@vality/ng-core'; import { FormControlSuperclass, Option, createControlProviders } from '@vality/ng-core';
import { BehaviorSubject, combineLatest, defer, Observable, of, switchMap } from 'rxjs'; import { BehaviorSubject, combineLatest, defer, Observable, of, switchMap } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators'; import { map, shareReplay } from 'rxjs/operators';

View File

@ -31,7 +31,7 @@ export class SidenavInfoService {
private sidenavInfoComponents?: SidenavInfoComponents, private sidenavInfoComponents?: SidenavInfoComponents,
) { ) {
if (!this.sidenavInfoComponents) { if (!this.sidenavInfoComponents) {
this.sidenavInfoComponents = sidenavInfoComponents = {}; this.sidenavInfoComponents = sidenavInfoComponents ?? {};
} }
router.events router.events
.pipe( .pipe(

View File

@ -0,0 +1,47 @@
<v-dialog
*ngIf="!isReview; else reviewTpl"
[progress]="!!(progress$ | async)"
title="Create domain object"
>
<cc-domain-thrift-form [formControl]="control" type="DomainObject"></cc-domain-thrift-form>
<v-dialog-actions>
<button
*ngIf="!isReview"
[disabled]="control.invalid"
color="primary"
mat-flat-button
(click)="isReview = true"
>
Review
</button>
</v-dialog-actions>
</v-dialog>
<ng-template #reviewTpl>
<v-dialog [progress]="!!(progress$ | async)" title="Review domain object">
<cc-domain-thrift-viewer
[value]="control.value"
kind="editor"
type="DomainObject"
></cc-domain-thrift-viewer>
<v-dialog-actions>
<button
*ngIf="isReview"
[disabled]="!!(progress$ | async)"
mat-flat-button
(click)="isReview = false"
>
Edit
</button>
<button
*ngIf="isReview"
[disabled]="!!(progress$ | async)"
color="primary"
mat-flat-button
(click)="create()"
>
Create
</button>
</v-dialog-actions>
</v-dialog>
</ng-template>

View File

@ -0,0 +1,109 @@
import { CommonModule } from '@angular/common';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { DomainObject } from '@vality/domain-proto/domain';
import {
DialogSuperclass,
DEFAULT_DIALOG_CONFIG,
DialogModule,
DEFAULT_DIALOG_CONFIG_FULL_HEIGHT,
DialogConfig,
progressTo,
NotifyLogService,
} from '@vality/ng-core';
import { BehaviorSubject, switchMap, EMPTY } from 'rxjs';
import { first, map, catchError } from 'rxjs/operators';
import { ValuesType } from 'utility-types';
import { getUnionKey } from '../../../../../../utils';
import { DomainStoreService } from '../../../../../api/domain-config';
import { DomainNavigateService } from '../../../../../sections/domain/services/domain-navigate.service';
import { MetadataService } from '../../../../../sections/domain/services/metadata.service';
import { DomainThriftFormComponent } from '../domain-thrift-form';
import { DomainThriftViewerComponent } from '../domain-thrift-viewer';
@Component({
selector: 'cc-create-domain-object-dialog',
standalone: true,
imports: [
CommonModule,
DialogModule,
MatButtonModule,
DomainThriftFormComponent,
ReactiveFormsModule,
DomainThriftViewerComponent,
],
templateUrl: './create-domain-object-dialog.component.html',
})
export class CreateDomainObjectDialogComponent
extends DialogSuperclass<CreateDomainObjectDialogComponent, { objectType?: string } | void>
implements OnInit
{
static defaultDialogConfig: ValuesType<DialogConfig> = {
...DEFAULT_DIALOG_CONFIG.large,
minHeight: DEFAULT_DIALOG_CONFIG_FULL_HEIGHT,
};
control = new FormControl<DomainObject | null>(null, [Validators.required]);
progress$ = new BehaviorSubject(0);
isReview = false;
constructor(
private domainStoreService: DomainStoreService,
private destroyRef: DestroyRef,
private log: NotifyLogService,
private domainNavigateService: DomainNavigateService,
private metadataService: MetadataService,
) {
super();
}
ngOnInit() {
if (this.dialogData) {
this.metadataService
.getDomainFieldByType(this.dialogData.objectType)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((type) => {
this.control.setValue({ [type.name]: {} });
});
}
}
create(attempts = 1) {
this.domainStoreService
.commit({ ops: [{ insert: { object: this.control.value } }] })
.pipe(
catchError((err) => {
if (err?.name === 'ObsoleteCommitVersion' && attempts !== 0) {
this.domainStoreService.forceReload();
this.create(attempts - 1);
this.log.error(err, `Domain config is out of date, one more attempt...`);
return EMPTY;
}
throw err;
}),
switchMap(() => this.getType()),
progressTo(this.progress$),
takeUntilDestroyed(this.destroyRef),
)
.subscribe({
next: (type) => {
this.log.successOperation('create', 'domain object');
void this.domainNavigateService.toType(String(type));
this.closeWithSuccess();
},
error: (err) => {
this.log.errorOperation(err, 'create', 'domain object');
},
});
}
private getType() {
return this.metadataService.getDomainFieldByName(getUnionKey(this.control.value)).pipe(
map((f) => String(f.type)),
first(),
);
}
}

View File

@ -0,0 +1 @@
export * from './create-domain-object-dialog.component';

View File

@ -3,8 +3,8 @@ import { Component, DestroyRef, Input, OnChanges } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { Reference } from '@vality/domain-proto/internal/domain'; import { Reference } from '@vality/domain-proto/internal/domain';
import { ComponentChanges } from '@vality/ng-core'; import { ComponentChanges, DialogService } from '@vality/ng-core';
import { combineLatest, ReplaySubject } from 'rxjs'; import { combineLatest, ReplaySubject, switchMap } from 'rxjs';
import { map, shareReplay, first } from 'rxjs/operators'; import { map, shareReplay, first } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config'; import { DomainStoreService } from '@cc/app/api/domain-config';
@ -13,6 +13,7 @@ import { toJson } from '@cc/utils';
import { SidenavInfoModule } from '../../../sidenav-info'; import { SidenavInfoModule } from '../../../sidenav-info';
import { CardComponent } from '../../../sidenav-info/components/card/card.component'; import { CardComponent } from '../../../sidenav-info/components/card/card.component';
import { DomainThriftViewerComponent } from '../domain-thrift-viewer'; import { DomainThriftViewerComponent } from '../domain-thrift-viewer';
import { EditDomainObjectDialogComponent } from '../edit-domain-object-dialog';
import { DomainObjectService } from '../services'; import { DomainObjectService } from '../services';
import { getDomainObjectDetails } from '../utils'; import { getDomainObjectDetails } from '../utils';
@ -50,6 +51,7 @@ export class DomainObjectCardComponent implements OnChanges {
private domainObjectService: DomainObjectService, private domainObjectService: DomainObjectService,
private domainStoreService: DomainStoreService, private domainStoreService: DomainStoreService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private dialogService: DialogService,
) {} ) {}
ngOnChanges(changes: ComponentChanges<DomainObjectCardComponent>) { ngOnChanges(changes: ComponentChanges<DomainObjectCardComponent>) {
@ -59,7 +61,17 @@ export class DomainObjectCardComponent implements OnChanges {
} }
edit() { edit() {
void this.domainObjectService.edit(this.ref); this.domainObject$
.pipe(
first(),
switchMap((domainObject) =>
this.dialogService
.open(EditDomainObjectDialogComponent, { domainObject })
.afterClosed(),
),
takeUntilDestroyed(this.destroyRef),
)
.subscribe();
} }
delete() { delete() {

View File

@ -1,4 +1,5 @@
<cc-thrift-editor <cc-thrift-editor
[defaultValue]="defaultValue"
[extensions]="extensions$ | async" [extensions]="extensions$ | async"
[formControl]="control" [formControl]="control"
[metadata]="metadata$ | async" [metadata]="metadata$ | async"

View File

@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { ThriftAstMetadata } from '@vality/fistful-proto'; import { ThriftAstMetadata } from '@vality/fistful-proto';
import { createControlProviders, getImportValue } from '@vality/ng-core'; import { getImportValue, createControlProviders } from '@vality/ng-core';
import { DomainMetadataFormExtensionsService } from '../../../../services'; import { DomainMetadataFormExtensionsService } from '../../../../services';
import { MetadataFormModule } from '../../../metadata-form'; import { MetadataFormModule } from '../../../metadata-form';

View File

@ -1,5 +1,7 @@
<cc-thrift-viewer <cc-thrift-viewer
[compared]="compared"
[extensions]="extensions$ | async" [extensions]="extensions$ | async"
[kind]="kind"
[metadata]="metadata$ | async" [metadata]="metadata$ | async"
[progress]="progress" [progress]="progress"
[type]="type" [type]="type"

View File

@ -4,7 +4,7 @@ import { ThriftAstMetadata } from '@vality/domain-proto';
import { getImportValue } from '@vality/ng-core'; import { getImportValue } from '@vality/ng-core';
import { ValueType } from '@vality/thrift-ts'; import { ValueType } from '@vality/thrift-ts';
import { ThriftViewerModule } from '../../../thrift-viewer'; import { ThriftViewerModule, ViewerKind } from '../../../thrift-viewer';
import { DomainMetadataViewExtensionsService } from './services/domain-metadata-view-extensions'; import { DomainMetadataViewExtensionsService } from './services/domain-metadata-view-extensions';
@ -15,7 +15,9 @@ import { DomainMetadataViewExtensionsService } from './services/domain-metadata-
imports: [CommonModule, ThriftViewerModule], imports: [CommonModule, ThriftViewerModule],
}) })
export class DomainThriftViewerComponent<T> { export class DomainThriftViewerComponent<T> {
@Input() kind: ViewerKind = ViewerKind.Component;
@Input() value: T; @Input() value: T;
@Input() compared?: T;
@Input() type: ValueType; @Input() type: ValueType;
@Input({ transform: booleanAttribute }) progress: boolean = false; @Input({ transform: booleanAttribute }) progress: boolean = false;
// @Input() extensions?: MetadataViewExtension[]; // @Input() extensions?: MetadataViewExtension[];

View File

@ -0,0 +1,86 @@
<ng-container [ngSwitch]="step">
<v-dialog
*ngSwitchCase="stepEnum.Edit"
[progress]="!!(progress$ | async)"
title="Edit domain object"
>
<cc-domain-thrift-form
[defaultValue]="(dialogData.domainObject | ccUnionValue)?.data"
[formControl]="control"
[type]="dataType$ | async"
></cc-domain-thrift-form>
<v-dialog-actions
><ng-container *ngTemplateOutlet="actions"></ng-container
></v-dialog-actions>
</v-dialog>
<v-dialog
*ngSwitchCase="stepEnum.Review"
[progress]="!!(progress$ | async)"
title="Review domain object"
>
<cc-domain-thrift-viewer
[compared]="newObject$ | async"
[type]="dataType$ | async"
[value]="currentObject ?? dialogData.domainObject"
kind="editor"
></cc-domain-thrift-viewer>
<v-dialog-actions
><ng-container *ngTemplateOutlet="actions"></ng-container
></v-dialog-actions>
</v-dialog>
<v-dialog
*ngSwitchCase="stepEnum.SourceReview"
[progress]="!!(progress$ | async)"
title="Changes from the server"
>
<cc-domain-thrift-viewer
*ngIf="currentObject"
[compared]="this.currentObject"
[type]="dataType$ | async"
[value]="dialogData.domainObject"
kind="editor"
></cc-domain-thrift-viewer>
<v-dialog-actions
><ng-container *ngTemplateOutlet="actions"></ng-container
></v-dialog-actions>
</v-dialog>
</ng-container>
<ng-template #actions>
<button
*ngIf="step !== stepEnum.Edit"
[disabled]="!!(progress$ | async)"
mat-flat-button
(click)="step = stepEnum.Edit"
>
Edit
</button>
<button
*ngIf="!!currentObject && step !== stepEnum.SourceReview"
[disabled]="!!(progress$ | async)"
mat-flat-button
(click)="step = stepEnum.SourceReview"
>
Review changes from the server
</button>
<button
*ngIf="step !== stepEnum.Review"
[disabled]="!allowReview"
color="primary"
mat-flat-button
(click)="step = stepEnum.Review"
>
Review
</button>
<button
*ngIf="step === stepEnum.Review"
[disabled]="!!(progress$ | async)"
color="primary"
mat-flat-button
(click)="update()"
>
Update
</button>
</ng-template>

View File

@ -0,0 +1,173 @@
import { CommonModule } from '@angular/common';
import { Component, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule, FormControl, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { DomainObject } from '@vality/domain-proto/domain';
import {
DialogSuperclass,
DEFAULT_DIALOG_CONFIG,
DialogModule,
DEFAULT_DIALOG_CONFIG_FULL_HEIGHT,
DialogConfig,
NotifyLogService,
getValueChanges,
progressTo,
DialogService,
ConfirmDialogComponent,
} from '@vality/ng-core';
import { BehaviorSubject, switchMap, EMPTY } from 'rxjs';
import { first, map, shareReplay, catchError } from 'rxjs/operators';
import { ValuesType } from 'utility-types';
import { getUnionKey, getUnionValue, isEqualThrift } from '../../../../../../utils';
import { DomainStoreService } from '../../../../../api/domain-config';
import { DomainNavigateService } from '../../../../../sections/domain/services/domain-navigate.service';
import { MetadataService } from '../../../../../sections/domain/services/metadata.service';
import { ThriftPipesModule } from '../../../../pipes';
import { DomainThriftFormComponent } from '../domain-thrift-form';
import { DomainThriftViewerComponent } from '../domain-thrift-viewer';
enum Step {
Edit,
Review,
SourceReview,
}
@Component({
selector: 'cc-edit-domain-object-dialog',
standalone: true,
imports: [
CommonModule,
DialogModule,
MatButtonModule,
DomainThriftFormComponent,
ReactiveFormsModule,
DomainThriftViewerComponent,
ThriftPipesModule,
],
templateUrl: './edit-domain-object-dialog.component.html',
})
export class EditDomainObjectDialogComponent extends DialogSuperclass<
EditDomainObjectDialogComponent,
{ domainObject: DomainObject }
> {
static defaultDialogConfig: ValuesType<DialogConfig> = {
...DEFAULT_DIALOG_CONFIG.large,
minHeight: DEFAULT_DIALOG_CONFIG_FULL_HEIGHT,
};
control = new FormControl<ValuesType<DomainObject>['data']>(
getUnionValue(this.dialogData.domainObject).data,
[Validators.required],
);
progress$ = new BehaviorSubject(0);
step: Step = Step.Edit;
stepEnum = Step;
currentObject?: DomainObject;
type$ = this.metadataService
.getDomainFieldByName(getUnionKey(this.dialogData.domainObject))
.pipe(
map((f) => String(f.type)),
first(),
);
dataType$ = this.metadataService
.getDomainObjectDataFieldByName(getUnionKey(this.dialogData.domainObject))
.pipe(
map((f) => String(f.type)),
first(),
);
newObject$ = getValueChanges(this.control).pipe(
map(() => this.getNewObject()),
shareReplay({ refCount: true, bufferSize: 1 }),
);
get allowReview() {
return (
this.control.valid &&
!isEqualThrift(this.currentObject ?? this.dialogData.domainObject, this.getNewObject())
);
}
constructor(
private domainStoreService: DomainStoreService,
private destroyRef: DestroyRef,
private log: NotifyLogService,
private domainNavigateService: DomainNavigateService,
private metadataService: MetadataService,
private dialogService: DialogService,
) {
super();
}
update(attempts = 1) {
this.domainStoreService
.getObject({
[getUnionKey(this.dialogData.domainObject)]: getUnionValue(
this.dialogData.domainObject,
).ref,
})
.pipe(
first(),
switchMap((currentObject) => {
if (!isEqualThrift(currentObject, this.dialogData.domainObject)) {
this.dialogService.open(ConfirmDialogComponent, {
title: 'The object has been modified',
description:
'The original object has been modified. View changes in the original object before committing your own.',
confirmLabel: 'View',
});
this.step = Step.SourceReview;
this.currentObject = currentObject;
return EMPTY;
} else if (this.currentObject) {
this.currentObject = undefined;
}
return this.domainStoreService.commit({
ops: [
{
update: {
old_object: currentObject,
new_object: this.getNewObject(),
},
},
],
});
}),
catchError((err) => {
if (err?.name === 'ObsoleteCommitVersion' && attempts !== 0) {
this.domainStoreService.forceReload();
this.update(attempts - 1);
this.log.error(err, `Domain config is out of date, one more attempt...`);
return EMPTY;
}
throw err;
}),
switchMap(() => this.type$),
progressTo(this.progress$),
takeUntilDestroyed(this.destroyRef),
)
.subscribe({
next: (type) => {
this.log.successOperation('update', 'domain object');
void this.domainNavigateService.toType(type);
this.closeWithSuccess();
},
error: (err) => {
this.log.errorOperation(err, 'update', 'domain object');
},
});
}
private getNewObject() {
return {
[getUnionKey(this.dialogData.domainObject)]: {
ref: getUnionValue(this.dialogData.domainObject).ref,
data: this.control.value,
},
};
}
}

View File

@ -0,0 +1 @@
export * from './edit-domain-object-dialog.component';

View File

@ -2,5 +2,8 @@ export * from './domain-thrift-viewer';
export * from './domain-object-field'; export * from './domain-object-field';
export * from './domain-thrift-form-dialog'; export * from './domain-thrift-form-dialog';
export * from './domain-object-card/domain-object-card.component'; export * from './domain-object-card/domain-object-card.component';
export * from './domain-thrift-form';
export * from './utils'; export * from './utils';
export * from './services'; export * from './services';
export * from './create-domain-object-dialog';
export * from './edit-domain-object-dialog';

View File

@ -1,6 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { DomainObject } from '@vality/domain-proto/domain';
import { DomainObject, Reference } from '@vality/domain-proto/domain';
import { import {
ConfirmDialogComponent, ConfirmDialogComponent,
DialogResponseStatus, DialogResponseStatus,
@ -16,18 +15,11 @@ import { DomainStoreService } from '../../../../../api/domain-config';
}) })
export class DomainObjectService { export class DomainObjectService {
constructor( constructor(
private router: Router,
private dialogService: DialogService, private dialogService: DialogService,
private domainStoreService: DomainStoreService, private domainStoreService: DomainStoreService,
private log: NotifyLogService, private log: NotifyLogService,
) {} ) {}
edit(ref: Reference) {
return this.router.navigate(['domain', 'edit'], {
queryParams: { ref: JSON.stringify(ref) },
});
}
delete(domainObject: DomainObject) { delete(domainObject: DomainObject) {
return this.dialogService return this.dialogService
.open(ConfirmDialogComponent, { title: 'Delete object' }) .open(ConfirmDialogComponent, { title: 'Delete object' })

View File

@ -8,13 +8,14 @@ import { map, shareReplay } from 'rxjs/operators';
import { MetadataFormExtension } from '../../../metadata-form'; import { MetadataFormExtension } from '../../../metadata-form';
@Directive() @Directive()
export abstract class BaseThriftFormSuperclass export abstract class BaseThriftFormSuperclass<T = unknown>
extends FormControlSuperclass<unknown> extends FormControlSuperclass<T>
implements OnChanges implements OnChanges
{ {
@Input() type: ValueType; @Input() type: ValueType;
@Input() namespace?: string; @Input() namespace?: string;
@Input() extensions?: MetadataFormExtension[]; @Input() extensions?: MetadataFormExtension[];
@Input() defaultValue?: T;
protected abstract defaultNamespace: string; protected abstract defaultNamespace: string;
protected abstract metadata$: Observable<ThriftAstMetadata[]>; protected abstract metadata$: Observable<ThriftAstMetadata[]>;

View File

@ -1,9 +1,29 @@
<div class="wrapper"> <div class="wrapper">
<div class="actions"> <div class="actions">
<button color="warn" mat-icon-button (click)="reset()"> <button
*ngIf="control.invalid"
[matTooltip]="control.errors | inlineJson: false"
color="warn"
mat-icon-button
(click)="control.updateValueAndValidity()"
>
<mat-icon>priority_high</mat-icon>
</button>
<button
*ngIf="control.dirty"
color="warn"
mat-icon-button
matTooltip="Reset"
(click)="reset()"
>
<mat-icon>restart_alt</mat-icon> <mat-icon>restart_alt</mat-icon>
</button> </button>
<button color="primary" mat-icon-button (click)="toggleKind()"> <button
color="primary"
mat-icon-button
matTooltip="Show {{ kind === 'form' ? 'form' : 'JSON' }} editor"
(click)="toggleKind()"
>
<mat-icon *ngIf="kind === 'form'">code</mat-icon> <mat-icon *ngIf="kind === 'form'">code</mat-icon>
<mat-icon *ngIf="kind === 'editor'">edit_note</mat-icon> <mat-icon *ngIf="kind === 'editor'">edit_note</mat-icon>
</button> </button>

View File

@ -1,11 +1,16 @@
import { Component, Input, Output, EventEmitter } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import { ValidationErrors } from '@angular/forms'; import { ValidationErrors, AbstractControl } from '@angular/forms';
import { ThriftAstMetadata } from '@vality/domain-proto'; import { ThriftAstMetadata } from '@vality/domain-proto';
import { DialogService, DialogResponseStatus, ConfirmDialogComponent } from '@vality/ng-core'; import {
DialogService,
DialogResponseStatus,
ConfirmDialogComponent,
createControlProviders,
FormControlSuperclass,
} from '@vality/ng-core';
import { merge, defer, of, Subject } from 'rxjs'; import { merge, defer, of, Subject } from 'rxjs';
import { map, filter, shareReplay } from 'rxjs/operators'; import { map, filter, shareReplay } from 'rxjs/operators';
import { ValidatedFormControlSuperclass, createControlProviders } from '@cc/utils';
import { objectToJSON } from '@cc/utils/thrift-instance'; import { objectToJSON } from '@cc/utils/thrift-instance';
import { MetadataFormExtension } from '../metadata-form'; import { MetadataFormExtension } from '../metadata-form';
@ -21,7 +26,7 @@ export enum EditorKind {
styleUrls: ['./thrift-editor.component.scss'], styleUrls: ['./thrift-editor.component.scss'],
providers: createControlProviders(() => ThriftEditorComponent), providers: createControlProviders(() => ThriftEditorComponent),
}) })
export class ThriftEditorComponent<T> extends ValidatedFormControlSuperclass<T> { export class ThriftEditorComponent<T> extends FormControlSuperclass<T> {
@Input() kind: EditorKind = EditorKind.Form; @Input() kind: EditorKind = EditorKind.Form;
@Input() defaultValue?: T; @Input() defaultValue?: T;
@ -49,11 +54,11 @@ export class ThriftEditorComponent<T> extends ValidatedFormControlSuperclass<T>
super(); super();
} }
validate(): ValidationErrors | null { validate(control: AbstractControl): ValidationErrors | null {
if (this.kind === EditorKind.Editor) { if (this.kind === EditorKind.Editor) {
return this.editorError ? { jsonParse: this.editorError } : null; return this.editorError ? { jsonParse: this.editorError } : null;
} }
return super.validate(); return super.validate(control);
} }
contentChange(str: string) { contentChange(str: string) {

View File

@ -4,6 +4,8 @@ import { ReactiveFormsModule, FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { PipesModule } from '@vality/ng-core';
import { MonacoEditorModule } from 'ngx-monaco-editor-v2'; import { MonacoEditorModule } from 'ngx-monaco-editor-v2';
import { MetadataFormModule } from '@cc/app/shared/components/metadata-form'; import { MetadataFormModule } from '@cc/app/shared/components/metadata-form';
@ -22,6 +24,8 @@ import { ThriftEditorComponent } from './thrift-editor.component';
MatButtonModule, MatButtonModule,
MatIconModule, MatIconModule,
FormsModule, FormsModule,
PipesModule,
MatTooltipModule,
], ],
}) })
export class ThriftEditorModule {} export class ThriftEditorModule {}

View File

@ -12,15 +12,13 @@ import {
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Validator, ValidationErrors, FormControl } from '@angular/forms'; import { Validator, ValidationErrors, FormControl } from '@angular/forms';
import { createMask } from '@ngneat/input-mask'; import { createMask } from '@ngneat/input-mask';
import { FormComponentSuperclass } from '@s-libs/ng-core'; import { FormComponentSuperclass, createControlProviders, getValueChanges } from '@vality/ng-core';
import sortBy from 'lodash-es/sortBy'; import sortBy from 'lodash-es/sortBy';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { map, switchMap, first, distinctUntilChanged } from 'rxjs/operators'; import { map, switchMap, first, distinctUntilChanged } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config'; import { DomainStoreService } from '@cc/app/api/domain-config';
import { createControlProviders, getFormValueChanges } from '../../utils';
export interface Cash { export interface Cash {
amount: number; amount: number;
currencyCode: string; currencyCode: string;
@ -42,7 +40,7 @@ export class CashFieldComponent extends FormComponentSuperclass<Cash> implements
currencyCodeControl = new FormControl<string>(null); currencyCodeControl = new FormControl<string>(null);
currencies$ = combineLatest([ currencies$ = combineLatest([
getFormValueChanges(this.currencyCodeControl, true), getValueChanges(this.currencyCodeControl),
this.domainStoreService.getObjects('currency'), this.domainStoreService.getObjects('currency'),
]).pipe( ]).pipe(
map(([code, currencies]) => map(([code, currencies]) =>
@ -53,7 +51,7 @@ export class CashFieldComponent extends FormComponentSuperclass<Cash> implements
), ),
); );
amountMask$ = getFormValueChanges(this.currencyCodeControl, true).pipe( amountMask$ = getValueChanges(this.currencyCodeControl).pipe(
switchMap((code) => this.getCurrencyByCode(code)), switchMap((code) => this.getCurrencyByCode(code)),
map((c) => (this.minor ? 0 : c?.data?.exponent || 2)), map((c) => (this.minor ? 0 : c?.data?.exponent || 2)),
distinctUntilChanged(), distinctUntilChanged(),
@ -84,8 +82,8 @@ export class CashFieldComponent extends FormComponentSuperclass<Cash> implements
ngOnInit() { ngOnInit() {
combineLatest([ combineLatest([
getFormValueChanges(this.currencyCodeControl, true), getValueChanges(this.currencyCodeControl),
getFormValueChanges(this.amountControl, true), getValueChanges(this.amountControl),
]) ])
.pipe( .pipe(
switchMap(([currencyCode]) => this.getCurrencyByCode(currencyCode)), switchMap(([currencyCode]) => this.getCurrencyByCode(currencyCode)),

View File

@ -1,18 +0,0 @@
import { AbstractControl, FormControlState } from '@angular/forms';
import { Observable } from 'rxjs';
import { startWith, map } from 'rxjs/operators';
import { getValue } from './get-value';
/**
* @deprecated
*/
export function getFormValueChanges<T>(
form: AbstractControl<FormControlState<T> | T>,
hasStart = false,
): Observable<T> {
return form.valueChanges.pipe(
...((hasStart ? [startWith(form.value)] : []) as []),
map(() => getValue(form)),
) as Observable<T>;
}

View File

@ -1,21 +0,0 @@
import { AbstractControl } from '@angular/forms';
import { hasControls } from './has-controls';
export function getValue<T extends AbstractControl>(control: T): T['value'] {
if (!hasControls(control)) {
return control.value as never;
}
if (Array.isArray(control.controls)) {
const result: T[] = [];
for (const v of control.controls) {
result.push(getValue(v) as T);
}
return result;
}
const result: Partial<T> = {};
for (const [k, v] of Object.entries(control.controls)) {
result[k] = getValue(v) as T;
}
return result;
}

View File

@ -1,5 +0,0 @@
import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
export function hasControls(control: AbstractControl): control is FormGroup | FormArray {
return 'controls' in control;
}

View File

@ -1,2 +0,0 @@
export * from './get-form-value-changes';
export * from './validated-control-superclass';

View File

@ -1,13 +0,0 @@
import { Provider, Type } from '@angular/core';
import { provideValidators } from './provide-validators';
import { provideValueAccessor } from './provide-value-accessor';
/**
* @deprecated
* @param component
*/
export const createControlProviders = (component: () => Type<unknown>): Provider[] => [
provideValueAccessor(component),
provideValidators(component),
];

View File

@ -1,6 +0,0 @@
export * from './validated-control-superclass.directive';
export * from './provide-validators';
export * from './provide-value-accessor';
export * from './create-control-providers';
export * from './utils/get-errors-tree';
export * from './validated-form-control-superclass.directive';

View File

@ -1,12 +0,0 @@
import { Provider, forwardRef, Type } from '@angular/core';
import { NG_VALIDATORS } from '@angular/forms';
/**
* @deprecated
* @param component
*/
export const provideValidators = (component: () => Type<unknown>): Provider => ({
provide: NG_VALIDATORS,
useExisting: forwardRef(component),
multi: true,
});

View File

@ -1,12 +0,0 @@
import { Provider, forwardRef, Type } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
/**
* @deprecated
* @param component
*/
export const provideValueAccessor = (component: () => Type<unknown>): Provider => ({
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(component),
multi: true,
});

View File

@ -1,29 +0,0 @@
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { hasControls } from '../../has-controls';
/**
* @deprecated
* FormGroup/FormArray don't return internal control errors,
* so you need to get internal errors manually
*/
export function getErrorsTree(control: AbstractControl): ValidationErrors | null {
if (control.valid) {
return null;
}
const errors: ValidationErrors = Object.assign({}, control.errors);
if (hasControls(control)) {
if (Array.isArray(control.controls)) {
errors['formArrayErrors'] = control.controls
.map((c: AbstractControl) => getErrorsTree(c))
.filter(Boolean);
} else {
errors['formGroupErrors'] = Object.fromEntries(
Array.from(Object.entries(control.controls))
.map(([k, c]) => [k, getErrorsTree(c)])
.filter(([, v]) => !!v),
) as ValidationErrors;
}
}
return errors;
}

View File

@ -1,54 +0,0 @@
import { Directive, OnInit } from '@angular/core';
import { FormGroup, ValidationErrors, Validator } from '@angular/forms';
import { WrappedControlSuperclass } from '@s-libs/ng-core';
import { EMPTY, Observable } from 'rxjs';
import { getValue } from '../get-value';
import { getErrorsTree } from './utils/get-errors-tree';
/**
* @deprecated
*/
@Directive()
export abstract class ValidatedControlSuperclass<OuterType, InnerType = OuterType>
extends WrappedControlSuperclass<OuterType, InnerType>
implements OnInit, Validator
{
protected emptyValue: InnerType;
ngOnInit() {
this.emptyValue = getValue(this.control);
super.ngOnInit();
}
validate(): ValidationErrors | null {
return getErrorsTree(this.control);
}
protected setUpOuterToInnerErrors$(
_outer$: Observable<ValidationErrors>,
): Observable<ValidationErrors> {
return EMPTY;
}
protected setUpInnerToOuterErrors$(
_inner$: Observable<ValidationErrors>,
): Observable<ValidationErrors> {
return EMPTY;
}
protected outerToInnerValue(outer: OuterType): InnerType {
if ('controls' in this.control) {
if (!outer) {
return this.emptyValue;
}
if (
Object.keys(outer).length < Object.keys((this.control as FormGroup).controls).length
) {
return Object.assign({}, this.emptyValue, outer);
}
}
return outer as never;
}
}

View File

@ -1,32 +0,0 @@
import { Directive } from '@angular/core';
import { ValidationErrors, FormControl } from '@angular/forms';
import { WrappedControlSuperclass } from '@s-libs/ng-core';
import { EMPTY, Observable } from 'rxjs';
/**
* @deprecated
*/
@Directive()
export class ValidatedFormControlSuperclass<
OuterType,
InnerType = OuterType,
> extends WrappedControlSuperclass<OuterType, InnerType> {
// TODO: Validation sometimes doesn't work (is not forwarded higher by nesting) with Angular FormControl
control = new FormControl() as FormControl<InnerType>;
validate(): ValidationErrors | null {
return this.control.errors;
}
protected setUpOuterToInnerErrors$(
_outer$: Observable<ValidationErrors>,
): Observable<ValidationErrors> {
return EMPTY;
}
protected setUpInnerToOuterErrors$(
_inner$: Observable<ValidationErrors>,
): Observable<ValidationErrors> {
return EMPTY;
}
}

View File

@ -7,7 +7,6 @@ export * from './to-major';
export * from './thrift-utils'; export * from './thrift-utils';
export * from './has-active-fragments'; export * from './has-active-fragments';
export * from './poll'; export * from './poll';
export * from './forms';
export * from './operators'; export * from './operators';
export * from './get-enum-keys'; export * from './get-enum-keys';
export * from './enumerate'; export * from './enumerate';

View File

@ -1 +1,2 @@
export * from './create-union'; export * from './create-union';
export * from './is-equal-thrift';

View File

@ -0,0 +1,5 @@
import { toJson } from '../thrift-json-converter';
export function isEqualThrift(a: unknown, b: unknown) {
return JSON.stringify(toJson(a)) === JSON.stringify(toJson(b));
}