IMP-32: New predicates in payment routing rules (#91)

This commit is contained in:
Rinat Arsaev 2022-05-30 20:44:04 +03:00 committed by GitHub
parent 53ac6091e8
commit ed44c4c608
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 661 additions and 1017 deletions

View File

@ -1,6 +0,0 @@
{
"include": "node_modules/@rbkmoney/angular-templates/**",
"template": {
"prefix": "cc"
}
}

View File

@ -34,7 +34,6 @@ import { PaymentAdjustmentModule } from './sections/payment-adjustment/payment-a
import { PayoutsModule } from './sections/payouts'; import { PayoutsModule } from './sections/payouts';
import { SearchClaimsModule } from './sections/search-claims/search-claims.module'; import { SearchClaimsModule } from './sections/search-claims/search-claims.module';
import { SearchPartiesModule } from './sections/search-parties/search-parties.module'; import { SearchPartiesModule } from './sections/search-parties/search-parties.module';
import { SettingsModule } from './settings';
import { ThemeManager, ThemeManagerModule, ThemeName } from './theme-manager'; import { ThemeManager, ThemeManagerModule, ThemeName } from './theme-manager';
import { import {
DEFAULT_DIALOG_CONFIG, DEFAULT_DIALOG_CONFIG,
@ -69,7 +68,6 @@ moment.locale('en');
DomainModule, DomainModule,
RepairingModule, RepairingModule,
ThemeManagerModule, ThemeManagerModule,
SettingsModule,
PartyModule, PartyModule,
SearchPartiesModule, SearchPartiesModule,
SearchClaimsModule, SearchClaimsModule,

View File

@ -1,7 +1,7 @@
<cc-metadata-form <cc-metadata-form
[formControl]="control" [formControl]="control"
[metadata]="metadata$ | async" [metadata]="metadata$ | async"
[extensions]="extensions" [extensions]="extensions$ | async"
namespace="claim_management" namespace="claim_management"
[type]="type" [type]="type"
></cc-metadata-form> ></cc-metadata-form>

View File

@ -3,19 +3,19 @@ import { ValidationErrors, Validator } from '@angular/forms';
import { WrappedFormControlSuperclass } from '@s-libs/ng-core'; import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { Claim } from '@vality/domain-proto/lib/claim_management'; import { Claim } from '@vality/domain-proto/lib/claim_management';
import { Party } from '@vality/domain-proto/lib/domain'; import { Party } from '@vality/domain-proto/lib/domain';
import { from } from 'rxjs'; import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ComponentChanges, MetadataFormExtension } from '@cc/app/shared'; import { ComponentChanges, MetadataFormExtension } from '@cc/app/shared';
import { createValidatedAbstractControlProviders } from '@cc/utils'; import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services/domain-metadata-form-extensions';
import { createControlProviders } from '@cc/utils';
import { DomainStoreService } from '../../../../thrift-services/damsel/domain-store.service';
import { createDomainObjectMetadataFormExtension } from './utils/create-domain-object-metadata-form.extension';
import { createPartyClaimMetadataFormExtensions } from './utils/create-party-claim-metadata-form-extensions'; import { createPartyClaimMetadataFormExtensions } from './utils/create-party-claim-metadata-form-extensions';
@Component({ @Component({
selector: 'cc-modification-form', selector: 'cc-modification-form',
templateUrl: './modification-form.component.html', templateUrl: './modification-form.component.html',
providers: createValidatedAbstractControlProviders(ModificationFormComponent), providers: createControlProviders(ModificationFormComponent),
}) })
export class ModificationFormComponent export class ModificationFormComponent
extends WrappedFormControlSuperclass<unknown> extends WrappedFormControlSuperclass<unknown>
@ -26,37 +26,28 @@ export class ModificationFormComponent
@Input() type: string; @Input() type: string;
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default)); metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions: MetadataFormExtension[]; extensions$: Observable<MetadataFormExtension[]>;
constructor(injector: Injector, private domainStoreService: DomainStoreService) { constructor(
injector: Injector,
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService
) {
super(injector); super(injector);
} }
ngOnChanges(changes: ComponentChanges<ModificationFormComponent>) { ngOnChanges(changes: ComponentChanges<ModificationFormComponent>) {
super.ngOnChanges(changes); super.ngOnChanges(changes);
if (changes.party || changes.claim) { if (changes.party || changes.claim) {
this.extensions = [ this.extensions$ = this.domainMetadataFormExtensionsService.extensions$.pipe(
map((e) => [
...createPartyClaimMetadataFormExtensions(this.party, this.claim), ...createPartyClaimMetadataFormExtensions(this.party, this.claim),
...this.createDomainMetadataFormExtensions(), ...e,
]; ])
);
} }
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.control.errors; return this.control.errors;
} }
private createDomainMetadataFormExtensions(): MetadataFormExtension[] {
return [
createDomainObjectMetadataFormExtension('ContractTemplateRef', () =>
this.domainStoreService.getObjects('contract_template')
),
createDomainObjectMetadataFormExtension('PaymentInstitutionRef', () =>
this.domainStoreService.getObjects('payment_institution')
),
createDomainObjectMetadataFormExtension('CategoryRef', () =>
this.domainStoreService.getObjects('category')
),
];
}
} }

View File

@ -1,6 +1,4 @@
<div fxLayout="column" fxLayoutGap="32px"> <cc-base-dialog title="Change Delegate Ruleset">
<div class="cc-headline">Change Delegate Ruleset</div>
<div [formGroup]="form" fxLayout="column" fxLayoutGap="24px"> <div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<mat-form-field> <mat-form-field>
<mat-label>Delegate Ruleset</mat-label> <mat-label>Delegate Ruleset</mat-label>
@ -16,10 +14,9 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div fxLayout fxLayoutAlign="space-between"> <cc-base-dialog-actions>
<button mat-button (click)="cancel()">CANCEL</button>
<button mat-button color="primary" (click)="changeRuleset()" [disabled]="form.invalid"> <button mat-button color="primary" (click)="changeRuleset()" [disabled]="form.invalid">
CHANGE RULESET CHANGE RULESET
</button> </button>
</div> </cc-base-dialog-actions>
</div> </cc-base-dialog>

View File

@ -1,9 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { RoutingRulesService } from '../../../thrift-services'; import { RoutingRulesService } from '../../../thrift-services';
@UntilDestroy() @UntilDestroy()
@ -12,7 +13,13 @@ import { RoutingRulesService } from '../../../thrift-services';
templateUrl: 'change-delegate-ruleset-dialog.component.html', templateUrl: 'change-delegate-ruleset-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ChangeDelegateRulesetDialogComponent implements OnInit { export class ChangeDelegateRulesetDialogComponent
extends BaseDialogSuperclass<
ChangeDelegateRulesetDialogComponent,
{ mainRulesetRefID: number; delegateIdx: number }
>
implements OnInit
{
form = this.fb.group({ form = this.fb.group({
rulesetRefId: [], rulesetRefId: [],
description: '', description: '',
@ -21,17 +28,18 @@ export class ChangeDelegateRulesetDialogComponent implements OnInit {
rulesets$ = this.routingRulesService.rulesets$; rulesets$ = this.routingRulesService.rulesets$;
constructor( constructor(
injector: Injector,
private fb: FormBuilder, private fb: FormBuilder,
private dialogRef: MatDialogRef<ChangeDelegateRulesetDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { mainRulesetRefID: number; delegateIdx: number },
private routingRulesService: RoutingRulesService private routingRulesService: RoutingRulesService
) {} ) {
super(injector);
}
ngOnInit() { ngOnInit() {
this.routingRulesService this.routingRulesService
.getRuleset(this.data.mainRulesetRefID) .getRuleset(this.dialogData.mainRulesetRefID)
.pipe( .pipe(
map((r) => r?.data?.decisions?.delegates?.[this.data?.delegateIdx]), map((r) => r?.data?.decisions?.delegates?.[this.dialogData?.delegateIdx]),
untilDestroyed(this) untilDestroyed(this)
) )
.subscribe((delegate) => { .subscribe((delegate) => {
@ -42,15 +50,11 @@ export class ChangeDelegateRulesetDialogComponent implements OnInit {
}); });
} }
cancel() {
this.dialogRef.close();
}
changeRuleset() { changeRuleset() {
this.routingRulesService this.routingRulesService
.changeDelegateRuleset({ .changeDelegateRuleset({
mainRulesetRefID: this.data.mainRulesetRefID, mainRulesetRefID: this.dialogData.mainRulesetRefID,
delegateIdx: this.data.delegateIdx, delegateIdx: this.dialogData.delegateIdx,
newDelegateRulesetRefID: this.form.value.rulesetRefId, newDelegateRulesetRefID: this.form.value.rulesetRefId,
description: this.form.value.description, description: this.form.value.description,
}) })

View File

@ -6,6 +6,8 @@ import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { BaseDialogModule } from '@cc/components/base-dialog';
import { ChangeDelegateRulesetDialogComponent } from './change-delegate-ruleset-dialog.component'; import { ChangeDelegateRulesetDialogComponent } from './change-delegate-ruleset-dialog.component';
@NgModule({ @NgModule({
@ -17,6 +19,7 @@ import { ChangeDelegateRulesetDialogComponent } from './change-delegate-ruleset-
MatInputModule, MatInputModule,
MatButtonModule, MatButtonModule,
MatSelectModule, MatSelectModule,
BaseDialogModule,
], ],
declarations: [ChangeDelegateRulesetDialogComponent], declarations: [ChangeDelegateRulesetDialogComponent],
exports: [ChangeDelegateRulesetDialogComponent], exports: [ChangeDelegateRulesetDialogComponent],

View File

@ -1,14 +1,11 @@
<div fxLayout="column" fxLayoutGap="32px"> <cc-base-dialog title="Change main ruleset">
<div class="cc-headline">Change main ruleset</div>
<cc-target-ruleset-form <cc-target-ruleset-form
(valueChanges)="targetRuleset$.next($event)" (valueChanges)="targetRuleset$.next($event)"
[value]="initValue" [value]="initValue"
(valid)="targetRulesetValid$.next($event)" (valid)="targetRulesetValid$.next($event)"
></cc-target-ruleset-form> ></cc-target-ruleset-form>
<div fxLayout fxLayoutAlign="space-between"> <cc-base-dialog-actions>
<button mat-button (click)="cancel()">CANCEL</button>
<button <button
mat-button mat-button
color="primary" color="primary"
@ -17,5 +14,5 @@
> >
CHANGE TARGET CHANGE TARGET
</button> </button>
</div> </cc-base-dialog-actions>
</div> </cc-base-dialog>

View File

@ -1,8 +1,9 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Injector } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { ErrorService } from '../../../shared/services/error'; import { ErrorService } from '../../../shared/services/error';
import { RoutingRulesService } from '../../../thrift-services'; import { RoutingRulesService } from '../../../thrift-services';
import { TargetRuleset } from '../target-ruleset-form'; import { TargetRuleset } from '../target-ruleset-form';
@ -12,32 +13,36 @@ import { TargetRuleset } from '../target-ruleset-form';
templateUrl: 'change-target-dialog.component.html', templateUrl: 'change-target-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ChangeTargetDialogComponent { export class ChangeTargetDialogComponent extends BaseDialogSuperclass<
ChangeTargetDialogComponent,
{ mainRulesetRefID: number; delegateIdx: number }
> {
targetRuleset$ = new BehaviorSubject<TargetRuleset>(undefined); targetRuleset$ = new BehaviorSubject<TargetRuleset>(undefined);
targetRulesetValid$ = new BehaviorSubject<boolean>(undefined); targetRulesetValid$ = new BehaviorSubject<boolean>(undefined);
initValue: Partial<TargetRuleset> = {}; initValue: Partial<TargetRuleset> = {};
constructor( constructor(
private dialogRef: MatDialogRef<ChangeTargetDialogComponent>, injector: Injector,
private routingRulesService: RoutingRulesService, private routingRulesService: RoutingRulesService,
@Inject(MAT_DIALOG_DATA) public data: { mainRulesetRefID: number; delegateIdx: number },
private errorService: ErrorService private errorService: ErrorService
) { ) {
super(injector);
this.routingRulesService this.routingRulesService
.getRuleset(data?.mainRulesetRefID) .getRuleset(this.dialogData?.mainRulesetRefID)
.pipe(untilDestroyed(this)) .pipe(untilDestroyed(this))
.subscribe((ruleset) => { .subscribe((ruleset) => {
this.initValue = { this.initValue = {
mainRulesetRefID: ruleset.ref.id, mainRulesetRefID: ruleset.ref.id,
mainDelegateDescription: mainDelegateDescription:
ruleset?.data?.decisions?.delegates?.[data?.delegateIdx]?.description, ruleset?.data?.decisions?.delegates?.[this.dialogData?.delegateIdx]
?.description,
}; };
}); });
} }
changeTarget() { changeTarget() {
const { mainRulesetRefID, mainDelegateDescription } = this.targetRuleset$.value; const { mainRulesetRefID, mainDelegateDescription } = this.targetRuleset$.value;
const { mainRulesetRefID: previousMainRulesetRefID, delegateIdx } = this.data; const { mainRulesetRefID: previousMainRulesetRefID, delegateIdx } = this.dialogData;
this.routingRulesService this.routingRulesService
.changeMainRuleset({ .changeMainRuleset({
previousMainRulesetRefID, previousMainRulesetRefID,
@ -48,8 +53,4 @@ export class ChangeTargetDialogComponent {
.pipe(untilDestroyed(this)) .pipe(untilDestroyed(this))
.subscribe(() => this.dialogRef.close(), this.errorService.error); .subscribe(() => this.dialogRef.close(), this.errorService.error);
} }
cancel() {
this.dialogRef.close();
}
} }

View File

@ -4,18 +4,19 @@ import { FlexLayoutModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { ErrorModule } from '../../../shared/services/error'; import { BaseDialogModule } from '@cc/components/base-dialog';
import { TargetRulesetFormModule } from '../target-ruleset-form'; import { TargetRulesetFormModule } from '../target-ruleset-form';
import { ChangeTargetDialogComponent } from './change-target-dialog.component'; import { ChangeTargetDialogComponent } from './change-target-dialog.component';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
ErrorModule,
TargetRulesetFormModule, TargetRulesetFormModule,
FlexLayoutModule, FlexLayoutModule,
MatDialogModule, MatDialogModule,
MatButtonModule, MatButtonModule,
BaseDialogModule,
], ],
declarations: [ChangeTargetDialogComponent], declarations: [ChangeTargetDialogComponent],
exports: [ChangeTargetDialogComponent], exports: [ChangeTargetDialogComponent],

View File

@ -1,7 +1,5 @@
<form [formGroup]="form" fxLayout="column" fxLayoutGap="32px"> <cc-base-dialog title="Attach party delegate ruleset">
<div class="cc-headline">Attach party delegate ruleset</div> <div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<div fxLayout="column" fxLayoutGap="24px">
<cc-target-ruleset-form <cc-target-ruleset-form
(valueChanges)="targetRuleset$.next($event)" (valueChanges)="targetRuleset$.next($event)"
(valid)="targetRulesetValid$.next($event)" (valid)="targetRulesetValid$.next($event)"
@ -25,8 +23,7 @@
</ng-container> </ng-container>
</div> </div>
<div fxLayout fxLayoutAlign="space-between"> <cc-base-dialog-actions>
<button mat-button (click)="cancel()">CANCEL</button>
<button <button
mat-button mat-button
color="primary" color="primary"
@ -35,5 +32,5 @@
> >
ATTACH ATTACH
</button> </button>
</div> </cc-base-dialog-actions>
</form> </cc-base-dialog>

View File

@ -1,9 +1,10 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Injector } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { ErrorService } from '../../../../shared/services/error'; import { ErrorService } from '../../../../shared/services/error';
import { RoutingRulesService } from '../../../../thrift-services'; import { RoutingRulesService } from '../../../../thrift-services';
import { TargetRuleset } from '../../target-ruleset-form'; import { TargetRuleset } from '../../target-ruleset-form';
@ -13,7 +14,10 @@ import { TargetRuleset } from '../../target-ruleset-form';
templateUrl: 'attach-new-ruleset-dialog.component.html', templateUrl: 'attach-new-ruleset-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AttachNewRulesetDialogComponent { export class AttachNewRulesetDialogComponent extends BaseDialogSuperclass<
AttachNewRulesetDialogComponent,
{ partyID: string }
> {
form = this.fb.group({ form = this.fb.group({
ruleset: this.fb.group({ ruleset: this.fb.group({
name: 'submain ruleset[by shop id]', name: 'submain ruleset[by shop id]',
@ -25,27 +29,27 @@ export class AttachNewRulesetDialogComponent {
targetRulesetValid$ = new BehaviorSubject<boolean>(undefined); targetRulesetValid$ = new BehaviorSubject<boolean>(undefined);
constructor( constructor(
injector: Injector,
private fb: FormBuilder, private fb: FormBuilder,
private dialogRef: MatDialogRef<AttachNewRulesetDialogComponent>,
private paymentRoutingRulesService: RoutingRulesService, private paymentRoutingRulesService: RoutingRulesService,
@Inject(MAT_DIALOG_DATA) public data: { partyID: string },
private errorService: ErrorService private errorService: ErrorService
) {} ) {
super(injector);
}
attach() { attach() {
const { mainRulesetRefID, mainDelegateDescription } = this.targetRuleset$.value; const { mainRulesetRefID, mainDelegateDescription } = this.targetRuleset$.value;
this.paymentRoutingRulesService this.paymentRoutingRulesService
.attachPartyDelegateRuleset({ .attachPartyDelegateRuleset({
partyID: this.data.partyID, partyID: this.dialogData.partyID,
mainRulesetRefID, mainRulesetRefID,
mainDelegateDescription, mainDelegateDescription,
ruleset: this.form.value.ruleset, ruleset: this.form.value.ruleset,
}) })
.pipe(untilDestroyed(this)) .pipe(untilDestroyed(this))
.subscribe(() => this.dialogRef.close(), this.errorService.error); .subscribe({
} next: () => this.dialogRef.close(),
error: (err) => this.errorService.error(err),
cancel() { });
this.dialogRef.close();
} }
} }

View File

@ -1,15 +1,15 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { first, map, switchMap, take } from 'rxjs/operators'; import { first, map, switchMap, take } from 'rxjs/operators';
import { BaseDialogService } from '@cc/components/base-dialog/services/base-dialog.service';
import { handleError } from '../../../../utils/operators/handle-error'; import { handleError } from '../../../../utils/operators/handle-error';
import { ErrorService } from '../../../shared/services/error'; import { ErrorService } from '../../../shared/services/error';
import { RoutingRulesService } from '../../../thrift-services'; import { RoutingRulesService } from '../../../thrift-services';
import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service'; import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service';
import { DialogConfig, DIALOG_CONFIG } from '../../../tokens';
import { AttachNewRulesetDialogComponent } from './attach-new-ruleset-dialog'; import { AttachNewRulesetDialogComponent } from './attach-new-ruleset-dialog';
import { PartyDelegateRulesetsService } from './party-delegate-rulesets.service'; import { PartyDelegateRulesetsService } from './party-delegate-rulesets.service';
@ -54,10 +54,9 @@ export class PartyDelegateRulesetsComponent {
private partyDelegateRulesetsService: PartyDelegateRulesetsService, private partyDelegateRulesetsService: PartyDelegateRulesetsService,
private paymentRoutingRulesService: RoutingRulesService, private paymentRoutingRulesService: RoutingRulesService,
private router: Router, private router: Router,
private dialog: MatDialog, private baseDialogService: BaseDialogService,
private domainStoreService: DomainStoreService, private domainStoreService: DomainStoreService,
private errorService: ErrorService, private errorService: ErrorService
@Inject(DIALOG_CONFIG) private dialogConfig: DialogConfig
) {} ) {}
attachNewRuleset() { attachNewRuleset() {
@ -65,11 +64,8 @@ export class PartyDelegateRulesetsComponent {
.pipe( .pipe(
take(1), take(1),
switchMap((partyID) => switchMap((partyID) =>
this.dialog this.baseDialogService
.open(AttachNewRulesetDialogComponent, { .open(AttachNewRulesetDialogComponent, { partyID })
...this.dialogConfig.medium,
data: { partyID },
})
.afterClosed() .afterClosed()
), ),
handleError(this.errorService.error), handleError(this.errorService.error),

View File

@ -15,9 +15,9 @@ import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { BaseDialogModule } from '@cc/components/base-dialog';
import { DetailsItemModule } from '@cc/components/details-item'; import { DetailsItemModule } from '@cc/components/details-item';
import { ErrorModule } from '../../../shared/services/error';
import { ChangeTargetDialogModule } from '../change-target-dialog'; import { ChangeTargetDialogModule } from '../change-target-dialog';
import { PaymentRoutingRulesetHeaderModule } from '../payment-routing-ruleset-header'; import { PaymentRoutingRulesetHeaderModule } from '../payment-routing-ruleset-header';
import { RoutingRulesListModule } from '../routing-rules-list'; import { RoutingRulesListModule } from '../routing-rules-list';
@ -48,10 +48,10 @@ const EXPORTED_DECLARATIONS = [PartyDelegateRulesetsComponent, AttachNewRulesetD
DetailsItemModule, DetailsItemModule,
MatInputModule, MatInputModule,
MatProgressBarModule, MatProgressBarModule,
ErrorModule,
ChangeTargetDialogModule, ChangeTargetDialogModule,
TargetRulesetFormModule, TargetRulesetFormModule,
RoutingRulesListModule, RoutingRulesListModule,
BaseDialogModule,
], ],
declarations: EXPORTED_DECLARATIONS, declarations: EXPORTED_DECLARATIONS,
exports: EXPORTED_DECLARATIONS, exports: EXPORTED_DECLARATIONS,

View File

@ -1,11 +1,9 @@
<form [formGroup]="form" fxLayout="column" fxLayoutGap="32px"> <cc-base-dialog title="Party payment routing rule params">
<div class="cc-headline">Party payment routing rule params</div> <div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<div fxLayout="column" fxLayoutGap="24px">
<mat-form-field> <mat-form-field>
<mat-label>Shop</mat-label> <mat-label>Shop</mat-label>
<mat-select formControlName="shopID" required> <mat-select formControlName="shopID" required>
<mat-option *ngFor="let shop of data.shops" [value]="shop.id"> <mat-option *ngFor="let shop of dialogData.shops" [value]="shop.id">
{{ shop.details.name }} {{ shop.details.name }}
</mat-option> </mat-option>
</mat-select> </mat-select>
@ -26,8 +24,7 @@
</div> </div>
</div> </div>
<div fxLayout fxLayoutAlign="space-between"> <cc-base-dialog-actions>
<button mat-button (click)="cancel()">CANCEL</button>
<button mat-button color="primary" (click)="add()" [disabled]="form.invalid">ADD</button> <button mat-button color="primary" (click)="add()" [disabled]="form.invalid">ADD</button>
</div> </cc-base-dialog-actions>
</form> </cc-base-dialog>

View File

@ -1,9 +1,10 @@
import { Component, Inject } from '@angular/core'; import { Component, Injector } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Shop } from '@vality/domain-proto/lib/domain'; import { Shop } from '@vality/domain-proto/lib/domain';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { ErrorService } from '../../../../shared/services/error'; import { ErrorService } from '../../../../shared/services/error';
import { RoutingRulesService } from '../../../../thrift-services'; import { RoutingRulesService } from '../../../../thrift-services';
@ -11,7 +12,10 @@ import { RoutingRulesService } from '../../../../thrift-services';
@Component({ @Component({
templateUrl: 'add-party-payment-routing-rule-dialog.component.html', templateUrl: 'add-party-payment-routing-rule-dialog.component.html',
}) })
export class AddPartyPaymentRoutingRuleDialogComponent { export class AddPartyPaymentRoutingRuleDialogComponent extends BaseDialogSuperclass<
AddPartyPaymentRoutingRuleDialogComponent,
{ refID: number; partyID: string; shops: Shop[] }
> {
form = this.fb.group({ form = this.fb.group({
shopID: '', shopID: '',
name: 'Ruleset[candidates]', name: 'Ruleset[candidates]',
@ -19,13 +23,13 @@ export class AddPartyPaymentRoutingRuleDialogComponent {
}); });
constructor( constructor(
injector: Injector,
private fb: FormBuilder, private fb: FormBuilder,
private dialogRef: MatDialogRef<AddPartyPaymentRoutingRuleDialogComponent>,
private paymentRoutingRulesService: RoutingRulesService, private paymentRoutingRulesService: RoutingRulesService,
@Inject(MAT_DIALOG_DATA)
public data: { refID: number; partyID: string; shops: Shop[] },
private errorService: ErrorService private errorService: ErrorService
) {} ) {
super(injector);
}
add() { add() {
const { shopID, name, description } = this.form.value; const { shopID, name, description } = this.form.value;
@ -33,15 +37,11 @@ export class AddPartyPaymentRoutingRuleDialogComponent {
.addShopRuleset({ .addShopRuleset({
name, name,
description, description,
partyRulesetRefID: this.data.refID, partyRulesetRefID: this.dialogData.refID,
partyID: this.data.partyID, partyID: this.dialogData.partyID,
shopID, shopID,
}) })
.pipe(untilDestroyed(this)) .pipe(untilDestroyed(this))
.subscribe(() => this.dialogRef.close(), this.errorService.error); .subscribe(() => this.dialogRef.close(), this.errorService.error);
} }
cancel() {
this.dialogRef.close();
}
} }

View File

@ -12,7 +12,8 @@ import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { ErrorModule } from '../../../../shared/services/error'; import { BaseDialogModule } from '@cc/components/base-dialog';
import { AddPartyPaymentRoutingRuleDialogComponent } from './add-party-payment-routing-rule-dialog.component'; import { AddPartyPaymentRoutingRuleDialogComponent } from './add-party-payment-routing-rule-dialog.component';
@NgModule({ @NgModule({
@ -29,7 +30,7 @@ import { AddPartyPaymentRoutingRuleDialogComponent } from './add-party-payment-r
MatSelectModule, MatSelectModule,
MatRadioModule, MatRadioModule,
MatAutocompleteModule, MatAutocompleteModule,
ErrorModule, BaseDialogModule,
], ],
declarations: [AddPartyPaymentRoutingRuleDialogComponent], declarations: [AddPartyPaymentRoutingRuleDialogComponent],
exports: [AddPartyPaymentRoutingRuleDialogComponent], exports: [AddPartyPaymentRoutingRuleDialogComponent],

View File

@ -1,7 +1,5 @@
<form [formGroup]="form" fxLayout="column" fxLayoutGap="32px"> <cc-base-dialog title="Payment rules init params">
<div class="cc-headline">Payment rules init params</div> <div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<div fxLayout="column" fxLayoutGap="24px">
<mat-form-field> <mat-form-field>
<input <input
matInput matInput
@ -25,8 +23,7 @@
</div> </div>
</div> </div>
<div fxLayout fxLayoutAlign="space-between"> <cc-base-dialog-actions>
<button mat-button (click)="cancel()">CANCEL</button>
<button mat-button color="primary" (click)="init()" [disabled]="form.invalid">INIT</button> <button mat-button color="primary" (click)="init()" [disabled]="form.invalid">INIT</button>
</div> </cc-base-dialog-actions>
</form> </cc-base-dialog>

View File

@ -1,8 +1,9 @@
import { Component, Inject } from '@angular/core'; import { Component, Injector } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { ErrorService } from '../../../../shared/services/error'; import { ErrorService } from '../../../../shared/services/error';
import { RoutingRulesService } from '../../../../thrift-services'; import { RoutingRulesService } from '../../../../thrift-services';
@ -11,7 +12,10 @@ import { RoutingRulesService } from '../../../../thrift-services';
selector: 'cc-initialize-payment-routing-rules-dialog', selector: 'cc-initialize-payment-routing-rules-dialog',
templateUrl: 'initialize-payment-routing-rules-dialog.component.html', templateUrl: 'initialize-payment-routing-rules-dialog.component.html',
}) })
export class InitializePaymentRoutingRulesDialogComponent { export class InitializePaymentRoutingRulesDialogComponent extends BaseDialogSuperclass<
InitializePaymentRoutingRulesDialogComponent,
{ partyID: string; refID: number }
> {
form = this.fb.group({ form = this.fb.group({
delegateDescription: 'Main delegate[party]', delegateDescription: 'Main delegate[party]',
name: 'submain ruleset[by shop id]', name: 'submain ruleset[by shop id]',
@ -19,28 +23,25 @@ export class InitializePaymentRoutingRulesDialogComponent {
}); });
constructor( constructor(
injector: Injector,
private fb: FormBuilder, private fb: FormBuilder,
private dialogRef: MatDialogRef<InitializePaymentRoutingRulesDialogComponent>,
private paymentRoutingRulesService: RoutingRulesService, private paymentRoutingRulesService: RoutingRulesService,
@Inject(MAT_DIALOG_DATA) public data: { partyID: string; refID: number },
private errorService: ErrorService private errorService: ErrorService
) {} ) {
super(injector);
}
init() { init() {
const { delegateDescription, name, description } = this.form.value; const { delegateDescription, name, description } = this.form.value;
this.paymentRoutingRulesService this.paymentRoutingRulesService
.addPartyRuleset({ .addPartyRuleset({
name, name,
partyID: this.data.partyID, partyID: this.dialogData.partyID,
mainRulesetRefID: this.data.refID, mainRulesetRefID: this.dialogData.refID,
description, description,
delegateDescription, delegateDescription,
}) })
.pipe(untilDestroyed(this)) .pipe(untilDestroyed(this))
.subscribe(() => this.dialogRef.close(), this.errorService.error); .subscribe(() => this.dialogRef.close(), this.errorService.error);
} }
cancel() {
this.dialogRef.close();
}
} }

View File

@ -12,7 +12,8 @@ import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { ErrorModule } from '../../../../shared/services/error'; import { BaseDialogModule } from '@cc/components/base-dialog';
import { InitializePaymentRoutingRulesDialogComponent } from './initialize-payment-routing-rules-dialog.component'; import { InitializePaymentRoutingRulesDialogComponent } from './initialize-payment-routing-rules-dialog.component';
@NgModule({ @NgModule({
@ -29,7 +30,7 @@ import { InitializePaymentRoutingRulesDialogComponent } from './initialize-payme
MatSelectModule, MatSelectModule,
MatRadioModule, MatRadioModule,
MatAutocompleteModule, MatAutocompleteModule,
ErrorModule, BaseDialogModule,
], ],
declarations: [InitializePaymentRoutingRulesDialogComponent], declarations: [InitializePaymentRoutingRulesDialogComponent],
exports: [InitializePaymentRoutingRulesDialogComponent], exports: [InitializePaymentRoutingRulesDialogComponent],

View File

@ -1,12 +1,12 @@
import { Component, Inject } from '@angular/core'; import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { filter, map, shareReplay, switchMap, take } from 'rxjs/operators'; import { filter, map, shareReplay, switchMap, take } from 'rxjs/operators';
import { BaseDialogService } from '@cc/components/base-dialog/services/base-dialog.service';
import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service'; import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service';
import { DialogConfig, DIALOG_CONFIG } from '../../../tokens';
import { AddPartyPaymentRoutingRuleDialogComponent } from './add-party-payment-routing-rule-dialog'; import { AddPartyPaymentRoutingRuleDialogComponent } from './add-party-payment-routing-rule-dialog';
import { InitializePaymentRoutingRulesDialogComponent } from './initialize-payment-routing-rules-dialog'; import { InitializePaymentRoutingRulesDialogComponent } from './initialize-payment-routing-rules-dialog';
import { PartyPaymentRoutingRulesetService } from './party-payment-routing-ruleset.service'; import { PartyPaymentRoutingRulesetService } from './party-payment-routing-ruleset.service';
@ -54,11 +54,10 @@ export class PaymentRoutingRulesComponent {
); );
constructor( constructor(
private dialog: MatDialog, private baseDialogService: BaseDialogService,
private partyPaymentRoutingRulesetService: PartyPaymentRoutingRulesetService, private partyPaymentRoutingRulesetService: PartyPaymentRoutingRulesetService,
private router: Router, private router: Router,
private domainStoreService: DomainStoreService, private domainStoreService: DomainStoreService
@Inject(DIALOG_CONFIG) private dialogConfig: DialogConfig
) {} ) {}
initialize() { initialize() {
@ -69,11 +68,8 @@ export class PaymentRoutingRulesComponent {
.pipe( .pipe(
take(1), take(1),
switchMap(([partyID, refID]) => switchMap(([partyID, refID]) =>
this.dialog this.baseDialogService
.open(InitializePaymentRoutingRulesDialogComponent, { .open(InitializePaymentRoutingRulesDialogComponent, { partyID, refID })
...this.dialogConfig.medium,
data: { partyID, refID },
})
.afterClosed() .afterClosed()
), ),
untilDestroyed(this) untilDestroyed(this)
@ -90,11 +86,8 @@ export class PaymentRoutingRulesComponent {
.pipe( .pipe(
take(1), take(1),
switchMap(([refID, shops, partyID]) => switchMap(([refID, shops, partyID]) =>
this.dialog this.baseDialogService
.open(AddPartyPaymentRoutingRuleDialogComponent, { .open(AddPartyPaymentRoutingRuleDialogComponent, { refID, shops, partyID })
...this.dialogConfig.medium,
data: { refID, shops, partyID },
})
.afterClosed() .afterClosed()
), ),
untilDestroyed(this) untilDestroyed(this)

View File

@ -18,7 +18,6 @@ import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ErrorModule } from '../../../shared/services/error';
import { DamselModule } from '../../../thrift-services'; import { DamselModule } from '../../../thrift-services';
import { ChangeTargetDialogModule } from '../change-target-dialog'; import { ChangeTargetDialogModule } from '../change-target-dialog';
import { PaymentRoutingRulesetHeaderModule } from '../payment-routing-ruleset-header'; import { PaymentRoutingRulesetHeaderModule } from '../payment-routing-ruleset-header';
@ -53,7 +52,6 @@ import { PaymentRoutingRulesComponent } from './party-payment-routing-ruleset.co
AddPartyPaymentRoutingRuleDialogModule, AddPartyPaymentRoutingRuleDialogModule,
InitializePaymentRoutingRulesDialogModule, InitializePaymentRoutingRulesDialogModule,
MatProgressBarModule, MatProgressBarModule,
ErrorModule,
ChangeTargetDialogModule, ChangeTargetDialogModule,
RoutingRulesListModule, RoutingRulesListModule,
], ],

View File

@ -2,12 +2,10 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter, EventEmitter,
Inject,
Input, Input,
Output, Output,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
@ -21,7 +19,6 @@ import { ConfirmActionDialogComponent } from '../../../../components/confirm-act
import { handleError } from '../../../../utils/operators/handle-error'; import { handleError } from '../../../../utils/operators/handle-error';
import { ErrorService } from '../../../shared/services/error'; import { ErrorService } from '../../../shared/services/error';
import { RoutingRulesService } from '../../../thrift-services'; import { RoutingRulesService } from '../../../thrift-services';
import { DIALOG_CONFIG, DialogConfig } from '../../../tokens';
import { ChangeDelegateRulesetDialogComponent } from '../change-delegate-ruleset-dialog'; import { ChangeDelegateRulesetDialogComponent } from '../change-delegate-ruleset-dialog';
import { ChangeTargetDialogComponent } from '../change-target-dialog'; import { ChangeTargetDialogComponent } from '../change-target-dialog';
@ -54,10 +51,7 @@ export class RoutingRulesListComponent<T extends { [N in PropertyKey]: any } & D
private paginator$ = new ReplaySubject<MatPaginator>(1); private paginator$ = new ReplaySubject<MatPaginator>(1);
// eslint-disable-next-line @typescript-eslint/member-ordering // eslint-disable-next-line @typescript-eslint/member-ordering
dataSource$ = combineLatest([ dataSource$ = combineLatest([this.data$, this.paginator$.pipe(startWith(null))]).pipe(
this.data$,
this.paginator$.pipe(startWith<any, null>(null)),
]).pipe(
map(([d, paginator]) => { map(([d, paginator]) => {
const data = new MatTableDataSource(d); const data = new MatTableDataSource(d);
data.paginator = paginator; data.paginator = paginator;
@ -81,25 +75,16 @@ export class RoutingRulesListComponent<T extends { [N in PropertyKey]: any } & D
} }
constructor( constructor(
private dialog: MatDialog,
private baseDialogService: BaseDialogService, private baseDialogService: BaseDialogService,
private errorService: ErrorService, private errorService: ErrorService,
private routingRulesService: RoutingRulesService, private routingRulesService: RoutingRulesService
@Inject(DIALOG_CONFIG) private dialogConfig: DialogConfig
) {} ) {}
getColumnsKeys(col) {
return col.key;
}
changeDelegateRuleset(delegateId: DelegateId) { changeDelegateRuleset(delegateId: DelegateId) {
this.dialog this.baseDialogService
.open(ChangeDelegateRulesetDialogComponent, { .open(ChangeDelegateRulesetDialogComponent, {
...this.dialogConfig.medium,
data: {
mainRulesetRefID: delegateId.parentRefId, mainRulesetRefID: delegateId.parentRefId,
delegateIdx: delegateId.delegateIdx, delegateIdx: delegateId.delegateIdx,
},
}) })
.afterClosed() .afterClosed()
.pipe(handleError(this.errorService.error), untilDestroyed(this)) .pipe(handleError(this.errorService.error), untilDestroyed(this))
@ -107,13 +92,10 @@ export class RoutingRulesListComponent<T extends { [N in PropertyKey]: any } & D
} }
changeTarget(delegateId: DelegateId) { changeTarget(delegateId: DelegateId) {
this.dialog this.baseDialogService
.open(ChangeTargetDialogComponent, { .open(ChangeTargetDialogComponent, {
...this.dialogConfig.medium,
data: {
mainRulesetRefID: delegateId.parentRefId, mainRulesetRefID: delegateId.parentRefId,
delegateIdx: delegateId.delegateIdx, delegateIdx: delegateId.delegateIdx,
},
}) })
.afterClosed() .afterClosed()
.pipe(untilDestroyed(this)) .pipe(untilDestroyed(this))

View File

@ -1,7 +1,5 @@
<form [formGroup]="form" fxLayout="column" fxLayoutGap="32px"> <cc-base-dialog title="Shop payment routing rule params">
<div class="cc-headline">Shop payment routing rule params</div> <div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<div fxLayout="column" fxLayoutGap="24px">
<div fxLayout="column" fxLayoutGap="16px"> <div fxLayout="column" fxLayoutGap="16px">
<mat-form-field> <mat-form-field>
<input <input
@ -32,10 +30,7 @@
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="cc-title">Predicate</div> <div class="cc-title">Predicate</div>
<cc-predicate <cc-predicate [formControl]="predicateControl"></cc-predicate>
(validationChange)="predicateValid = $event"
(predicateChange)="predicate = $event"
></cc-predicate>
<mat-divider></mat-divider> <mat-divider></mat-divider>
@ -115,15 +110,14 @@
</div> </div>
</div> </div>
<div fxLayout fxLayoutAlign="space-between"> <cc-base-dialog-actions>
<button mat-button (click)="cancel()">CANCEL</button>
<button <button
mat-button mat-button
color="primary" color="primary"
(click)="add()" (click)="add()"
[disabled]="form.invalid || !predicateValid" [disabled]="form.invalid || predicateControl.invalid"
> >
ADD ADD
</button> </button>
</div> </cc-base-dialog-actions>
</form> </cc-base-dialog>

View File

@ -1,43 +1,50 @@
import { Component, Inject } from '@angular/core'; import { Component, Injector } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Validators } from '@angular/forms';
import { FormBuilder } from '@ngneat/reactive-forms';
import { UntilDestroy } from '@ngneat/until-destroy';
import { Predicate, RiskScore } from '@vality/domain-proto/lib/domain'; import { Predicate, RiskScore } from '@vality/domain-proto/lib/domain';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { DomainStoreService } from '../../../../thrift-services/damsel/domain-store.service'; import { DomainStoreService } from '../../../../thrift-services/damsel/domain-store.service';
import { import {
AddShopPaymentRoutingRuleDialogService, AddShopPaymentRoutingRuleDialogService,
TerminalType, TerminalType,
} from './add-shop-payment-routing-rule-dialog.service'; } from './add-shop-payment-routing-rule-dialog.service';
@UntilDestroy()
@Component({ @Component({
selector: 'cc-add-shop-payment-routing-rule-dialog', selector: 'cc-add-shop-payment-routing-rule-dialog',
templateUrl: 'add-shop-payment-routing-rule-dialog.component.html', templateUrl: 'add-shop-payment-routing-rule-dialog.component.html',
styleUrls: ['add-shop-payment-routing-rule-dialog.component.scss'], styleUrls: ['add-shop-payment-routing-rule-dialog.component.scss'],
providers: [AddShopPaymentRoutingRuleDialogService], providers: [AddShopPaymentRoutingRuleDialogService],
}) })
export class AddShopPaymentRoutingRuleDialogComponent { export class AddShopPaymentRoutingRuleDialogComponent extends BaseDialogSuperclass<
AddShopPaymentRoutingRuleDialogComponent,
{ refID: number }
> {
form = this.addShopPaymentRoutingRuleDialogService.form; form = this.addShopPaymentRoutingRuleDialogService.form;
newTerminalOptionsForm = this.addShopPaymentRoutingRuleDialogService.newTerminalOptionsForm; newTerminalOptionsForm = this.addShopPaymentRoutingRuleDialogService.newTerminalOptionsForm;
predicateControl = this.fb.control<Predicate>(null, Validators.required);
terminalType = TerminalType; terminalType = TerminalType;
riskScore = RiskScore; riskScore = RiskScore;
terminals$ = this.domainStoreService.getObjects('terminal'); terminals$ = this.domainStoreService.getObjects('terminal');
predicate: Predicate;
predicateValid: boolean;
constructor( constructor(
injector: Injector,
private addShopPaymentRoutingRuleDialogService: AddShopPaymentRoutingRuleDialogService, private addShopPaymentRoutingRuleDialogService: AddShopPaymentRoutingRuleDialogService,
private dialogRef: MatDialogRef<AddShopPaymentRoutingRuleDialogComponent>,
private domainStoreService: DomainStoreService, private domainStoreService: DomainStoreService,
@Inject(MAT_DIALOG_DATA) public data: { partyID: string; refID: number } private fb: FormBuilder
) {} ) {
super(injector);
add() {
this.addShopPaymentRoutingRuleDialogService.add(this.predicate);
} }
cancel() { add() {
this.dialogRef.close(); this.addShopPaymentRoutingRuleDialogService.add(
this.predicateControl.value,
this.dialogData.refID
);
} }
addOption() { addOption() {

View File

@ -12,6 +12,9 @@ import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MetadataFormModule } from '@cc/app/shared';
import { BaseDialogModule } from '@cc/components/base-dialog';
import { AddShopPaymentRoutingRuleDialogComponent } from './add-shop-payment-routing-rule-dialog.component'; import { AddShopPaymentRoutingRuleDialogComponent } from './add-shop-payment-routing-rule-dialog.component';
import { ExpanderComponent } from './expander'; import { ExpanderComponent } from './expander';
import { PredicateComponent } from './predicate'; import { PredicateComponent } from './predicate';
@ -30,6 +33,8 @@ import { PredicateComponent } from './predicate';
MatSelectModule, MatSelectModule,
MatRadioModule, MatRadioModule,
MatAutocompleteModule, MatAutocompleteModule,
MetadataFormModule,
BaseDialogModule,
], ],
declarations: [AddShopPaymentRoutingRuleDialogComponent, PredicateComponent, ExpanderComponent], declarations: [AddShopPaymentRoutingRuleDialogComponent, PredicateComponent, ExpanderComponent],
exports: [AddShopPaymentRoutingRuleDialogComponent], exports: [AddShopPaymentRoutingRuleDialogComponent],

View File

@ -1,10 +1,12 @@
import { Inject, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FormArray, FormBuilder, Validators } from '@angular/forms'; import { FormArray, FormBuilder, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { Predicate } from '@vality/domain-proto/lib/domain'; import { Predicate } from '@vality/domain-proto/lib/domain';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { startWith, switchMap, take } from 'rxjs/operators'; import { startWith, switchMap, take } from 'rxjs/operators';
import { BaseDialogResponseStatus } from '@cc/components/base-dialog';
import { RoutingRulesService, TerminalService } from '../../../../thrift-services'; import { RoutingRulesService, TerminalService } from '../../../../thrift-services';
import { AddShopPaymentRoutingRuleDialogComponent } from './add-shop-payment-routing-rule-dialog.component'; import { AddShopPaymentRoutingRuleDialogComponent } from './add-shop-payment-routing-rule-dialog.component';
@ -37,8 +39,7 @@ export class AddShopPaymentRoutingRuleDialogService {
private fb: FormBuilder, private fb: FormBuilder,
private dialogRef: MatDialogRef<AddShopPaymentRoutingRuleDialogComponent>, private dialogRef: MatDialogRef<AddShopPaymentRoutingRuleDialogComponent>,
private paymentRoutingRulesService: RoutingRulesService, private paymentRoutingRulesService: RoutingRulesService,
private terminalService: TerminalService, private terminalService: TerminalService
@Inject(MAT_DIALOG_DATA) public data: { partyID: string; refID: number }
) { ) {
this.form this.form
.get('terminalType') .get('terminalType')
@ -62,7 +63,7 @@ export class AddShopPaymentRoutingRuleDialogService {
}); });
} }
add(predicate: Predicate) { add(predicate: Predicate, refID: number) {
const { description, weight, priority, terminalType, existentTerminalID, newTerminal } = const { description, weight, priority, terminalType, existentTerminalID, newTerminal } =
this.form.value; this.form.value;
(terminalType === TerminalType.New (terminalType === TerminalType.New
@ -82,12 +83,12 @@ export class AddShopPaymentRoutingRuleDialogService {
weight, weight,
priority, priority,
terminalID, terminalID,
refID: this.data.refID, refID,
predicate, predicate,
}) })
) )
) )
.subscribe(() => this.dialogRef.close()); .subscribe(() => this.dialogRef.close({ status: BaseDialogResponseStatus.Success }));
} }
addOption() { addOption() {

View File

@ -1,203 +1,7 @@
<div fxLayout="column" fxLayoutGap="24px" [formGroup]="form"> <cc-metadata-form
<mat-radio-group formControlName="type" fxLayout="column" fxLayoutGap="16px"> [formControl]="control"
<mat-radio-button [value]="predicateType.condition">Condition</mat-radio-button> [metadata]="metadata$ | async"
<div fxLayout> [extensions]="extensions$ | async"
<mat-radio-button [value]="predicateType.constant" fxFlex>Constant</mat-radio-button> namespace="domain"
<mat-radio-button [value]="predicateType.allOf" fxFlex>All of</mat-radio-button> type="Predicate"
</div> ></cc-metadata-form>
<div fxLayout>
<mat-radio-button [value]="predicateType.anyOf" fxFlex>Any of</mat-radio-button>
<mat-radio-button [value]="predicateType.isNot" fxFlex>Is not</mat-radio-button>
</div>
</mat-radio-group>
<div
*ngIf="
[predicateType.anyOf, predicateType.allOf, predicateType.isNot].includes(
form.value.type
) && childrenForm?.controls?.length
"
fxLayout
fxLayoutGap="24px"
>
<mat-divider vertical></mat-divider>
<div fxLayout="column" fxLayoutGap="24px" fxFlex>
<cc-expander
*ngIf="form.value.type === predicateType.isNot; else allNAnyOf"
title="Is not predicate"
(remove)="removeAll()"
>
<cc-predicate [form]="childrenForm.controls[0]"></cc-predicate>
</cc-expander>
<ng-template #allNAnyOf>
<cc-expander
*ngFor="let childForm of childrenForm.controls; let idx = index"
[title]="
(form.value.type === predicateType.anyOf ? 'Any' : 'All') +
' of predicate #' +
(idx + 1)
"
(remove)="removeChild(idx)"
>
<cc-predicate [form]="childForm"></cc-predicate>
</cc-expander>
<mat-icon class="action" fxFlexAlign="end" (click)="addChild()">add</mat-icon>
</ng-template>
</div>
</div>
<ng-container *ngIf="form.value.type === predicateType.constant">
<div class="cc-subheading-2">Constant</div>
<mat-radio-group formControlName="constant" fxLayout>
<mat-radio-button [value]="true" fxFlex>True</mat-radio-button>
<mat-radio-button [value]="false" fxFlex>False</mat-radio-button>
</mat-radio-group>
</ng-container>
<ng-container *ngIf="form.value.type === predicateType.condition" formGroupName="condition">
<div class="cc-subheading-2">Condition</div>
<mat-radio-group formControlName="type" fxLayout="column" fxLayoutGap="16px">
<mat-radio-button [value]="conditionType.paymentTool">Payment tool</mat-radio-button>
</mat-radio-group>
<ng-container
formGroupName="paymentTool"
*ngIf="form.value.condition.type === conditionType.paymentTool"
>
<div class="cc-subheading-2">Payment tool condition</div>
<mat-radio-group formControlName="type" fxLayout="column" fxLayoutGap="16px">
<mat-radio-button [value]="paymentToolType.bankCard">Bank Card</mat-radio-button>
</mat-radio-group>
<ng-container
formGroupName="bankCard"
*ngIf="form.value.condition.paymentTool.type === paymentToolType.bankCard"
>
<div class="cc-subheading-2">Bank card condition</div>
<mat-radio-group formControlName="type" fxLayout="column" fxLayoutGap="16px">
<div fxLayout>
<mat-radio-button [value]="bankCardType.issuerCountryIs" fxFlex
>Issuer country is</mat-radio-button
>
<mat-radio-button [value]="bankCardType.paymentSystem" fxFlex
>Payment system</mat-radio-button
>
</div>
<mat-radio-button [value]="bankCardType.paymentSystemIs"
>Payment system is (deprecated)</mat-radio-button
>
</mat-radio-group>
<div fxLayout="column">
<mat-form-field
*ngIf="
form.value.condition.paymentTool.bankCard.type ===
bankCardType.issuerCountryIs
"
>
<input
matInput
placeholder="Residence"
formControlName="residence"
[matAutocomplete]="residenceAuto"
required
/>
<mat-autocomplete autoActiveFirstOption #residenceAuto="matAutocomplete">
<mat-option *ngFor="let option of residences$ | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
<mat-hint>ISO_3166-1 Alpha-3 Code</mat-hint>
</mat-form-field>
<mat-form-field
*ngIf="
form.value.condition.paymentTool.bankCard.type ===
bankCardType.paymentSystemIs
"
>
<input
matInput
placeholder="Payment system"
formControlName="paymentSystemIs"
[matAutocomplete]="paymentSystemAuto"
required
/>
<mat-autocomplete
autoActiveFirstOption
#paymentSystemAuto="matAutocomplete"
>
<mat-option
*ngFor="let option of deprecatedPaymentSystems$ | async"
[value]="option"
>
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<ng-container
*ngIf="
form.value.condition.paymentTool.bankCard.type ===
bankCardType.paymentSystem
"
>
<mat-form-field>
<input
matInput
placeholder="Payment system"
formControlName="paymentSystem"
[matAutocomplete]="paymentSystemAuto"
required
/>
<mat-autocomplete
autoActiveFirstOption
#paymentSystemAuto="matAutocomplete"
>
<mat-option
*ngFor="let option of paymentSystems$ | async"
[value]="option.ref.id"
>
{{ option.data?.name }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="Token provider"
formControlName="tokenProvider"
[matAutocomplete]="tokenProviderAuto"
/>
<mat-autocomplete
autoActiveFirstOption
#tokenProviderAuto="matAutocomplete"
>
<mat-option
*ngFor="let option of tokenProviders$ | async"
[value]="option"
>
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="Tokenization method"
formControlName="tokenizationMethod"
[matAutocomplete]="tokenizationMethodAuto"
/>
<mat-autocomplete
autoActiveFirstOption
#tokenizationMethodAuto="matAutocomplete"
>
<mat-option
*ngFor="let option of tokenizationMethods$ | async"
[value]="option"
>
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</ng-container>
</div>
</ng-container>
</ng-container>
</ng-container>
</div>

View File

@ -1,302 +1,26 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { Component, Injector, OnChanges } from '@angular/core';
import { import { Predicate } from '@vality/domain-proto/lib/domain';
AbstractControl, import { from } from 'rxjs';
FormArray,
FormBuilder,
FormGroup,
ValidatorFn,
Validators,
} from '@angular/forms';
import {
BankCardConditionDefinition,
LegacyBankCardPaymentSystem,
LegacyBankCardTokenProvider,
Predicate,
CountryCode,
TokenizationMethod,
} from '@vality/domain-proto/lib/domain';
import identity from 'lodash-es/identity';
import pickBy from 'lodash-es/pickBy';
import { merge, Observable, Subscription } from 'rxjs';
import { distinctUntilChanged, map, shareReplay, startWith, tap } from 'rxjs/operators';
import { ComponentChanges } from '@cc/app/shared/utils'; import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { DomainStoreService } from '../../../../../thrift-services/damsel/domain-store.service';
/* eslint-disable @typescript-eslint/naming-convention */
enum PredicateType {
constant = 'constant',
condition = 'condition',
anyOf = 'anyOf',
allOf = 'allOf',
isNot = 'isNot',
}
enum ConditionType {
paymentTool = 'paymentTool',
}
enum PaymentToolType {
bankCard = 'bankCard',
}
enum BankCardType {
issuerCountryIs = 'issuerCountryIs',
paymentSystem = 'paymentSystem',
paymentSystemIs = 'paymentSystemIs',
}
/* eslint-enable @typescript-eslint/naming-convention */
@Component({ @Component({
selector: 'cc-predicate', selector: 'cc-predicate',
templateUrl: 'predicate.component.html', templateUrl: 'predicate.component.html',
styleUrls: ['predicate.component.scss'], providers: createControlProviders(PredicateComponent),
}) })
export class PredicateComponent implements OnChanges { export class PredicateComponent
@Input() form = this.createForm(); extends ValidatedFormControlSuperclass<Predicate>
@Output() validationChange = new EventEmitter<boolean>(); implements OnChanges
@Output() predicateChange = new EventEmitter<Predicate>();
predicateType = PredicateType;
conditionType = ConditionType;
paymentToolType = PaymentToolType;
bankCardType = BankCardType;
deprecatedPaymentSystems$: Observable<string[]>;
paymentSystems$ = this.domainStoreService.getObjects('payment_system');
tokenProviders$: Observable<string[]>;
tokenizationMethods$: Observable<string[]>;
residences$: Observable<string[]>;
private outputSub: Subscription;
get childrenForm() {
return this.form?.controls?.children as FormArray;
}
constructor(private fb: FormBuilder, private domainStoreService: DomainStoreService) {
this.init();
}
ngOnChanges({ form }: ComponentChanges<PredicateComponent>): void {
if (form) {
this.init(true);
}
}
addChild() {
this.childrenForm.push(this.createForm());
}
removeChild(idx: number) {
this.childrenForm.removeAt(idx);
if (!this.childrenForm.controls.length) {
this.addChild();
}
}
removeAll() {
this.childrenForm.clear();
this.addChild();
}
private init(isInternal = false) {
if (this.childrenForm && !this.childrenForm.controls.length) {
this.addChild();
}
const { condition, constant, type } = this.form.controls;
type.valueChanges.pipe(startWith(type.value), distinctUntilChanged()).subscribe((t) => {
switch (t) {
case PredicateType.allOf:
case PredicateType.anyOf:
case PredicateType.isNot:
this.childrenForm.enable();
constant.disable();
condition.disable();
break;
case PredicateType.constant:
this.childrenForm.disable();
constant.enable();
condition.disable();
break;
case PredicateType.condition:
this.childrenForm.disable();
constant.disable();
condition.enable();
break;
default:
this.childrenForm.disable();
constant.disable();
condition.disable();
break;
}
});
const {
residence,
paymentSystemIs,
paymentSystem,
type: bankCardType,
tokenProvider,
tokenizationMethod,
} = (this.form.get('condition.paymentTool.bankCard') as FormGroup).controls;
bankCardType.valueChanges
.pipe(startWith(bankCardType.value), distinctUntilChanged())
.subscribe((t) => {
switch (t) {
case BankCardType.issuerCountryIs:
paymentSystem.disable();
paymentSystemIs.disable();
residence.enable();
break;
case BankCardType.paymentSystem:
residence.disable();
paymentSystemIs.disable();
paymentSystem.enable();
break;
case BankCardType.paymentSystemIs:
residence.disable();
paymentSystem.disable();
paymentSystemIs.enable();
break;
default:
residence.disable();
paymentSystem.disable();
paymentSystemIs.disable();
break;
}
});
if (this.outputSub) {
this.outputSub.unsubscribe();
delete this.outputSub;
}
if (!isInternal) {
this.outputSub = merge(
this.form.valueChanges.pipe(
startWith(this.form.value),
map(() => this.valueToPredicate()),
distinctUntilChanged(),
tap((v) => this.predicateChange.next(v))
),
this.form.statusChanges.pipe(
startWith(this.form.status),
map(() => this.form.valid),
distinctUntilChanged(),
tap((s) => this.validationChange.next(s))
)
).subscribe();
}
this.deprecatedPaymentSystems$ = this.getFilteredKeys(
paymentSystemIs,
LegacyBankCardPaymentSystem
);
this.tokenProviders$ = this.getFilteredKeys(tokenProvider, LegacyBankCardTokenProvider);
this.tokenizationMethods$ = this.getFilteredKeys(tokenizationMethod, TokenizationMethod);
this.residences$ = this.getFilteredKeys(residence, CountryCode);
}
private createForm() {
return this.fb.group({
type: ['', Validators.required],
condition: this.fb.group({
type: [ConditionType.paymentTool, Validators.required],
paymentTool: this.fb.group({
type: [PaymentToolType.bankCard, Validators.required],
bankCard: this.fb.group({
type: ['', Validators.required],
residence: ['', [Validators.required, this.enumValidator(CountryCode)]],
paymentSystemIs: [
'',
[Validators.required, this.enumValidator(LegacyBankCardPaymentSystem)],
],
paymentSystem: ['', [Validators.required]],
tokenProvider: ['', this.enumValidator(LegacyBankCardTokenProvider)],
tokenizationMethod: ['', this.enumValidator(TokenizationMethod)],
}),
}),
}),
constant: ['', Validators.required],
children: this.fb.array([]),
});
}
private valueToPredicate(value = this.form.value): Predicate {
if (this.form.invalid) {
return null;
}
switch (value.type) {
case PredicateType.allOf:
return { all_of: value.children.map((c) => this.valueToPredicate(c)) };
case PredicateType.anyOf:
return { any_of: value.children.map((c) => this.valueToPredicate(c)) };
case PredicateType.isNot:
return { is_not: this.valueToPredicate(value.children[0]) };
case PredicateType.constant:
return { constant: value.constant };
case PredicateType.condition:
if (!value.condition) {
return null;
}
return {
condition: {
payment_tool: {
bank_card: {
definition: this.bankCardValueToBankCardConditionDefinition(
value.condition.paymentTool.bankCard
),
},
},
},
};
}
}
private bankCardValueToBankCardConditionDefinition(value: any): BankCardConditionDefinition {
switch (value.type) {
case BankCardType.issuerCountryIs:
return { issuer_country_is: CountryCode[value.residence as string] };
case BankCardType.paymentSystem:
return {
payment_system: pickBy(
{ {
tokenization_method_is: metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
TokenizationMethod[value.tokenizationMethod as string], extensions$ = this.domainMetadataFormExtensionsService.extensions$;
payment_system_is: { id: value.paymentSystem },
token_provider_is_deprecated:
LegacyBankCardTokenProvider[value.tokenProvider as string],
},
identity
),
};
case BankCardType.paymentSystemIs:
return {
payment_system_is:
LegacyBankCardPaymentSystem[
value.paymentSystemIs as keyof LegacyBankCardPaymentSystem
],
};
}
}
private getFilteredKeys(control: AbstractControl, enumObj: any) { constructor(
return control.valueChanges.pipe( injector: Injector,
startWith(control.value), private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService
map((v) => v.trim().toLowerCase()), ) {
map((v) => super(injector);
this.getKeys(enumObj).filter((option) => option.toLowerCase().indexOf(v) === 0)
),
shareReplay(1)
);
}
private getKeys(enumObj: any) {
return Object.keys(enumObj).filter((k) => isNaN(+k));
}
private enumValidator(enumObj: any): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null =>
!control.value || Object.keys(enumObj).includes(control.value)
? null
: { enumNotIncludeKey: { value: control.value } };
} }
} }

View File

@ -1,21 +1,19 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Predicate, TerminalObject } from '@vality/domain-proto/lib/domain'; import { Predicate, TerminalObject } from '@vality/domain-proto/lib/domain';
import { combineLatest } from 'rxjs'; import { first, map, shareReplay, switchMap } from 'rxjs/operators';
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
import { objectToJSON } from '@cc/app/api/utils'; import { objectToJSON } from '@cc/app/api/utils';
import { NotificationService } from '@cc/app/shared/services/notification';
import { BaseDialogResponseStatus } from '@cc/components/base-dialog';
import { BaseDialogService } from '@cc/components/base-dialog/services/base-dialog.service';
import { handleError } from '../../../../utils/operators/handle-error';
import { ErrorService } from '../../../shared/services/error'; import { ErrorService } from '../../../shared/services/error';
import { damselInstanceToObject } from '../../../thrift-services'; import { damselInstanceToObject } from '../../../thrift-services';
import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service'; import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service';
import { AddShopPaymentRoutingRuleDialogComponent } from './add-shop-payment-routing-rule-dialog'; import { AddShopPaymentRoutingRuleDialogComponent } from './add-shop-payment-routing-rule-dialog';
import { ShopPaymentRoutingRulesetService } from './shop-payment-routing-ruleset.service'; import { ShopPaymentRoutingRulesetService } from './shop-payment-routing-ruleset.service';
const DIALOG_WIDTH = '548px';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
selector: 'cc-shop-payment-routing-ruleset', selector: 'cc-shop-payment-routing-ruleset',
@ -44,30 +42,40 @@ export class ShopPaymentRoutingRulesetComponent {
isLoading$ = this.domainStoreService.isLoading$; isLoading$ = this.domainStoreService.isLoading$;
constructor( constructor(
private dialog: MatDialog, private baseDialogService: BaseDialogService,
private shopPaymentRoutingRulesetService: ShopPaymentRoutingRulesetService, private shopPaymentRoutingRulesetService: ShopPaymentRoutingRulesetService,
private domainStoreService: DomainStoreService, private domainStoreService: DomainStoreService,
private errorService: ErrorService private errorService: ErrorService,
private notificationService: NotificationService
) {} ) {}
addShopRule() { addShopRule() {
combineLatest([this.partyID$, this.shopPaymentRoutingRulesetService.refID$]) this.shopPaymentRoutingRulesetService.refID$
.pipe( .pipe(
take(1), first(),
switchMap(([partyID, refID]) => switchMap((refID) =>
this.dialog this.baseDialogService
.open(AddShopPaymentRoutingRuleDialogComponent, { .open(AddShopPaymentRoutingRuleDialogComponent, { refID })
disableClose: true,
width: DIALOG_WIDTH,
maxHeight: '90vh',
data: { partyID, refID },
})
.afterClosed() .afterClosed()
),
handleError(this.errorService.error),
untilDestroyed(this)
) )
.subscribe(); )
.pipe(untilDestroyed(this))
.subscribe({
next: (res) => {
if (res.status === BaseDialogResponseStatus.Success) {
this.domainStoreService.forceReload();
this.notificationService.success(
'Shop payment routing ruleset successfully added'
);
}
},
error: (err) => {
this.errorService.error(err);
this.notificationService.success(
'Error while adding shop payment routing ruleset'
);
},
});
} }
removeShopRule(idx: number) { removeShopRule(idx: number) {

View File

@ -21,7 +21,6 @@ import { RouterModule } from '@angular/router';
import { PrettyJsonModule } from '@cc/components/pretty-json'; import { PrettyJsonModule } from '@cc/components/pretty-json';
import { ErrorModule } from '../../../shared/services/error';
import { DamselModule } from '../../../thrift-services'; import { DamselModule } from '../../../thrift-services';
import { PaymentRoutingRulesetHeaderModule } from '../payment-routing-ruleset-header'; import { PaymentRoutingRulesetHeaderModule } from '../payment-routing-ruleset-header';
import { AddShopPaymentRoutingRuleDialogModule } from './add-shop-payment-routing-rule-dialog'; import { AddShopPaymentRoutingRuleDialogModule } from './add-shop-payment-routing-rule-dialog';
@ -54,7 +53,6 @@ import { ShopPaymentRoutingRulesetComponent } from './shop-payment-routing-rules
AddShopPaymentRoutingRuleDialogModule, AddShopPaymentRoutingRuleDialogModule,
PrettyJsonModule, PrettyJsonModule,
MatProgressBarModule, MatProgressBarModule,
ErrorModule,
], ],
declarations: [ShopPaymentRoutingRulesetComponent], declarations: [ShopPaymentRoutingRulesetComponent],
}) })

View File

@ -10,13 +10,11 @@ import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { DetailsItemModule } from '../../../../components/details-item'; import { DetailsItemModule } from '../../../../components/details-item';
import { ErrorModule } from '../../../shared/services/error';
import { TargetRulesetFormComponent } from './target-ruleset-form.component'; import { TargetRulesetFormComponent } from './target-ruleset-form.component';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
ErrorModule,
FlexLayoutModule, FlexLayoutModule,
ReactiveFormsModule, ReactiveFormsModule,
MatRadioModule, MatRadioModule,

View File

@ -6,10 +6,7 @@ import { Party, Shop } from '@vality/magista-proto/lib/domain';
import { Moment } from 'moment'; import { Moment } from 'moment';
import * as moment from 'moment'; import * as moment from 'moment';
import { import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils/forms';
createValidatedAbstractControlProviders,
ValidatedWrappedAbstractControlSuperclass,
} from '@cc/utils/forms';
import { getEnumKeys } from '@cc/utils/get-enum-keys'; import { getEnumKeys } from '@cc/utils/get-enum-keys';
export interface PayoutsSearchForm { export interface PayoutsSearchForm {
@ -25,9 +22,9 @@ export interface PayoutsSearchForm {
@Component({ @Component({
selector: 'cc-payouts-search-form', selector: 'cc-payouts-search-form',
templateUrl: './payouts-search-form.component.html', templateUrl: './payouts-search-form.component.html',
providers: createValidatedAbstractControlProviders(PayoutsSearchFormComponent), providers: createControlProviders(PayoutsSearchFormComponent),
}) })
export class PayoutsSearchFormComponent extends ValidatedWrappedAbstractControlSuperclass<PayoutsSearchForm> { export class PayoutsSearchFormComponent extends ValidatedControlSuperclass<PayoutsSearchForm> {
control = this.fb.group<PayoutsSearchForm>({ control = this.fb.group<PayoutsSearchForm>({
payoutId: null, payoutId: null,
partyId: null, partyId: null,

View File

@ -1,2 +0,0 @@
export * from './settings.module';
export * from './settings.service';

View File

@ -1,8 +0,0 @@
import { NgModule } from '@angular/core';
import { SettingsService } from './settings.service';
@NgModule({
providers: [SettingsService],
})
export class SettingsModule {}

View File

@ -1,22 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable()
export class SettingsService {
set(key: string, value: string) {
localStorage.setItem(this.getKeyName(key), value);
}
setAll(keyValue: { [name: string]: string }) {
for (const [k, v] of Object.entries(keyValue)) {
this.set(k, v);
}
}
get(key: string): string {
return localStorage.getItem(this.getKeyName(key));
}
private getKeyName(name: string) {
return `cc-${name}`;
}
}

View File

@ -1,21 +1,17 @@
<div gdColumns="1fr" gdGap="16px"> <div gdColumns="1fr" gdGap="16px">
<span class="cc-body-1"> <span class="cc-body-1" *ngIf="hasLabel">
<cc-field-label [field]="data.field" [type]="data.type"></cc-field-label> <cc-field-label [field]="data.field" [type]="data.type"></cc-field-label>
({{ data.type.name | titlecase }}) ({{ data.type.name | titlecase }})
</span> </span>
<mat-accordion> <mat-accordion [ngStyle]="{ 'padding-left': hasLabel && '16px' }">
<mat-expansion-panel <mat-expansion-panel
class="mat-elevation-z0" class="mat-elevation-z0"
*ngFor="let control of controls.controls; let i = index" *ngFor="let valueControl of valueControls.controls; let i = index"
> >
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title fxLayoutAlign=" center"> <mat-panel-title fxLayoutAlign=" center">
{{ i + 1 }}. {{ i + 1 }}.
{{ {{ hasKeys ? (keyType | valueTypeTitle | titlecase) + ' - ' : '' }}
data.type.name === 'map'
? (data.type.keyType | valueTypeTitle | titlecase) + ' - '
: ''
}}
{{ data.type.valueType | valueTypeTitle | titlecase }} {{ data.type.valueType | valueTypeTitle | titlecase }}
</mat-panel-title> </mat-panel-title>
<mat-panel-description fxLayoutAlign="end"> <mat-panel-description fxLayoutAlign="end">
@ -25,18 +21,20 @@
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div gdColumns="1fr" gdGap="16px"> <div gdColumns="1fr" gdGap="16px">
<ng-container *ngIf="data.type.name === 'map'"> <ng-container *ngIf="hasKeys">
<span class="cc-body-2">Key</span> <span class="cc-body-2">Key</span>
<cc-metadata-form <cc-metadata-form
[formControl]="keyControls.controls[i]"
[metadata]="data.metadata" [metadata]="data.metadata"
[namespace]="data.namespace" [namespace]="data.namespace"
[type]="data.type.keyType" [type]="keyType"
[parent]="data" [parent]="data"
[extensions]="data.extensions" [extensions]="data.extensions"
></cc-metadata-form> ></cc-metadata-form>
</ng-container> </ng-container>
<span class="cc-body-2" *ngIf="data.type.name === 'map'">Value</span> <span class="cc-body-2" *ngIf="data.type.name === 'map'">Value</span>
<cc-metadata-form <cc-metadata-form
[formControl]="valueControl"
[metadata]="data.metadata" [metadata]="data.metadata"
[namespace]="data.namespace" [namespace]="data.namespace"
[type]="data.type.valueType" [type]="data.type.valueType"

View File

@ -1,5 +1,5 @@
mat-expansion-panel-body { ::ng-deep .mat-expansion-panel-body {
padding: 0; padding: 0 !important;
} }
mat-expansion-panel-header { mat-expansion-panel-header {

View File

@ -1,38 +1,75 @@
import { Component, Input } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { ValidationErrors, Validator } from '@angular/forms'; import { ValidationErrors, Validator } from '@angular/forms';
import { FormArray, FormControl } from '@ngneat/reactive-forms'; import { FormArray, FormControl } from '@ngneat/reactive-forms';
import { WrappedFormControlSuperclass } from '@s-libs/ng-core'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { FormComponentSuperclass } from '@s-libs/ng-core';
import { MapType, SetType, ListType } from '@vality/thrift-ts'; import { MapType, SetType, ListType } from '@vality/thrift-ts';
import { createValidatedAbstractControlProviders } from '@cc/utils'; import { createControlProviders, getErrorsTree } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
@UntilDestroy()
@Component({ @Component({
selector: 'cc-complex-form', selector: 'cc-complex-form',
templateUrl: './complex-form.component.html', templateUrl: './complex-form.component.html',
styleUrls: ['complex-form.component.scss'], styleUrls: ['complex-form.component.scss'],
providers: createValidatedAbstractControlProviders(ComplexFormComponent), providers: createControlProviders(ComplexFormComponent),
}) })
export class ComplexFormComponent export class ComplexFormComponent<T extends unknown[] | Map<unknown, unknown> | Set<unknown>>
extends WrappedFormControlSuperclass<unknown> extends FormComponentSuperclass<T>
implements Validator implements OnInit, Validator
{ {
@Input() data: MetadataFormData<SetType | MapType | ListType>; @Input() data: MetadataFormData<SetType | MapType | ListType>;
controls = new FormArray([]); valueControls = new FormArray([]);
keyControls = new FormArray([]);
add() { get hasLabel() {
this.controls.push(new FormControl()); return !!this.data.trueParent;
} }
delete(idx: number) { get hasKeys() {
this.controls.removeAt(idx); return this.data.type.name === 'map';
}
get keyType() {
if ('keyType' in this.data.type) return this.data.type.keyType;
}
ngOnInit() {
this.valueControls.valueChanges.pipe(untilDestroyed(this)).subscribe((value) => {
switch (this.data.type.name) {
case 'list':
this.emitOutgoingValue(value as never);
break;
case 'map':
this.emitOutgoingValue(
new Map(value.map((v, idx) => [this.keyControls.value[idx], v])) as never
);
break;
case 'set':
this.emitOutgoingValue(new Set(value) as never);
break;
}
});
}
handleIncomingValue(value: T) {
this.valueControls.patchValue(value as never, { emitEvent: false });
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.control.invalid || this.controls.invalid return getErrorsTree(this.keyControls) || getErrorsTree(this.valueControls);
? { [this.data.type.name + 'Invalid']: true } }
: null;
add() {
this.valueControls.push(new FormControl());
if (this.hasKeys) this.keyControls.push(new FormControl());
}
delete(idx: number) {
this.valueControls.removeAt(idx);
if (this.hasKeys) this.keyControls.removeAt(idx);
} }
} }

View File

@ -3,9 +3,12 @@
<cc-field-label [field]="data.field" [type]="data.type"></cc-field-label> <cc-field-label [field]="data.field" [type]="data.type"></cc-field-label>
</mat-label> </mat-label>
<mat-select [formControl]="control" [required]="data.isRequired"> <mat-select [formControl]="control" [required]="data.isRequired">
<mat-option *ngFor="let item of data.ast.items" [value]="item.value">{{ <mat-option
item.name *ngFor="let item of data.ast.items; let idx = index"
}}</mat-option> [value]="item.value ?? idx"
>
{{ item.name }}
</mat-option>
</mat-select> </mat-select>
<button <button
matSuffix matSuffix

View File

@ -1,21 +1,15 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { ValidationErrors, Validator } from '@angular/forms';
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { Enums } from '@vality/thrift-ts/src/thrift-parser'; import { Enums } from '@vality/thrift-ts/src/thrift-parser';
import { createValidatedAbstractControlProviders } from '@cc/utils'; import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
@Component({ @Component({
selector: 'cc-enum-field', selector: 'cc-enum-field',
templateUrl: './enum-field.component.html', templateUrl: './enum-field.component.html',
providers: createValidatedAbstractControlProviders(EnumFieldComponent), providers: createControlProviders(EnumFieldComponent),
}) })
export class EnumFieldComponent extends WrappedFormControlSuperclass<unknown> implements Validator { export class EnumFieldComponent<T> extends ValidatedFormControlSuperclass<T> {
@Input() data: MetadataFormData<string, Enums[string]>; @Input() data: MetadataFormData<string, Enums[string]>;
validate(): ValidationErrors | null {
return this.control.errors;
}
} }

View File

@ -1,4 +1,32 @@
<div gdColumns="1fr" gdGap="16px"> <div gdColumns="1fr" gdGap="16px">
<ng-container *ngIf="data.type === 'bool'; else input">
<div gdColumns="1fr" gdGap="16px">
<cc-field-label
class="cc-body-1"
[field]="data.field"
[type]="data.type"
></cc-field-label>
<div fxLayoutGap="4px" fxLayoutAlign=" center">
<mat-radio-group fxFlex gdColumns="1fr 1fr" gdGap="8px" [formControl]="control">
<mat-radio-button [value]="false">False</mat-radio-button>
<mat-radio-button [value]="true">True</mat-radio-button>
</mat-radio-group>
<button
mat-icon-button
*ngIf="
!data.isRequired && control.value !== null && control.value !== undefined
"
(click)="clear($event)"
>
<mat-icon>clear</mat-icon>
</button>
<button *ngIf="generate$ | async" mat-icon-button (click)="generate($event)">
<mat-icon>loop</mat-icon>
</button>
</div>
</div>
</ng-container>
<ng-template #input>
<div fxLayoutGap="4px"> <div fxLayoutGap="4px">
<mat-form-field fxFlex> <mat-form-field fxFlex>
<mat-label> <mat-label>
@ -14,7 +42,11 @@
[ngClass]="{ 'cc-code': (data.extensionResult$ | async)?.isIdentifier }" [ngClass]="{ 'cc-code': (data.extensionResult$ | async)?.isIdentifier }"
/> />
<div matSuffix fxLayoutGap="4px"> <div matSuffix fxLayoutGap="4px">
<button mat-icon-button *ngIf="control.value" (click)="clear($event)"> <button
mat-icon-button
*ngIf="!data.isRequired && control.value"
(click)="clear($event)"
>
<mat-icon>clear</mat-icon> <mat-icon>clear</mat-icon>
</button> </button>
<button <button
@ -49,6 +81,7 @@
<mat-icon>loop</mat-icon> <mat-icon>loop</mat-icon>
</button> </button>
</div> </div>
</ng-template>
<ng-container *ngIf="selected$ | async as selected"> <ng-container *ngIf="selected$ | async as selected">
<mat-expansion-panel *ngIf="selected.details"> <mat-expansion-panel *ngIf="selected.details">
<mat-expansion-panel-header> <mat-expansion-panel-header>

View File

@ -1,13 +1,11 @@
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { ValidationErrors, Validator } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { ThriftType } from '@vality/thrift-ts'; import { ThriftType } from '@vality/thrift-ts';
import { combineLatest, defer, ReplaySubject, switchMap } from 'rxjs'; import { combineLatest, defer, ReplaySubject, switchMap } from 'rxjs';
import { map, pluck, shareReplay, startWith } from 'rxjs/operators'; import { map, pluck, shareReplay, startWith } from 'rxjs/operators';
import { ComponentChanges, getAliases, getValueTypeTitle } from '@cc/app/shared'; import { ComponentChanges, getAliases, getValueTypeTitle } from '@cc/app/shared';
import { createValidatedAbstractControlProviders } from '@cc/utils'; import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
@ -15,11 +13,11 @@ import { MetadataFormData } from '../../types/metadata-form-data';
@Component({ @Component({
selector: 'cc-primitive-field', selector: 'cc-primitive-field',
templateUrl: './primitive-field.component.html', templateUrl: './primitive-field.component.html',
providers: createValidatedAbstractControlProviders(PrimitiveFieldComponent), providers: createControlProviders(PrimitiveFieldComponent),
}) })
export class PrimitiveFieldComponent export class PrimitiveFieldComponent<T>
extends WrappedFormControlSuperclass<unknown> extends ValidatedFormControlSuperclass<T>
implements OnChanges, Validator implements OnChanges
{ {
@Input() data: MetadataFormData<ThriftType>; @Input() data: MetadataFormData<ThriftType>;
@ -71,22 +69,18 @@ export class PrimitiveFieldComponent
private data$ = new ReplaySubject<MetadataFormData<ThriftType>>(1); private data$ = new ReplaySubject<MetadataFormData<ThriftType>>(1);
ngOnChanges(changes: ComponentChanges<PrimitiveFieldComponent>) { ngOnChanges(changes: ComponentChanges<PrimitiveFieldComponent<T>>) {
super.ngOnChanges(changes); super.ngOnChanges(changes);
if (changes.data) this.data$.next(this.data); if (changes.data) this.data$.next(this.data);
} }
validate(): ValidationErrors | null {
return this.control.errors;
}
generate(event: MouseEvent) { generate(event: MouseEvent) {
this.generate$ this.generate$
.pipe( .pipe(
switchMap((generate) => generate()), switchMap((generate) => generate()),
untilDestroyed(this) untilDestroyed(this)
) )
.subscribe((value) => this.control.setValue(value)); .subscribe((value) => this.control.setValue(value as T));
event.stopPropagation(); event.stopPropagation();
} }

View File

@ -1,5 +1,5 @@
<div gdColumns="1fr" gdGap="16px"> <div gdColumns="1fr" gdGap="16px">
<ng-container *ngIf="data.trueParent?.objectType !== 'union'"> <ng-container *ngIf="hasLabel">
<mat-checkbox *ngIf="!labelControl.disabled; else label" [formControl]="labelControl"> <mat-checkbox *ngIf="!labelControl.disabled; else label" [formControl]="labelControl">
<ng-container [ngTemplateOutlet]="label"></ng-container> <ng-container [ngTemplateOutlet]="label"></ng-container>
</mat-checkbox> </mat-checkbox>
@ -14,6 +14,7 @@
<ng-container *ngIf="labelControl.value"> <ng-container *ngIf="labelControl.value">
<cc-metadata-form <cc-metadata-form
*ngFor="let field of data.ast" *ngFor="let field of data.ast"
[ngStyle]="{ 'padding-left': hasLabel && '16px' }"
[formControl]="control.get(field.name)" [formControl]="control.get(field.name)"
[metadata]="data.metadata" [metadata]="data.metadata"
[namespace]="data.namespace" [namespace]="data.namespace"

View File

@ -1,15 +1,14 @@
import { Component, Injector, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; import { Component, Injector, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ValidationErrors, Validator, Validators } from '@angular/forms'; import { Validators } from '@angular/forms';
import { FormBuilder } from '@ngneat/reactive-forms'; import { FormBuilder } from '@ngneat/reactive-forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { FormComponentSuperclass } from '@s-libs/ng-core';
import { Field } from '@vality/thrift-ts'; import { Field } from '@vality/thrift-ts';
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 { merge } from 'rxjs';
import { delay } from 'rxjs/operators'; import { delay } from 'rxjs/operators';
import { createValidatedAbstractControlProviders } from '@cc/utils'; import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
@ -17,17 +16,25 @@ import { MetadataFormData } from '../../types/metadata-form-data';
@Component({ @Component({
selector: 'cc-struct-form', selector: 'cc-struct-form',
templateUrl: './struct-form.component.html', templateUrl: './struct-form.component.html',
providers: createValidatedAbstractControlProviders(StructFormComponent), providers: createControlProviders(StructFormComponent),
}) })
export class StructFormComponent export class StructFormComponent<T extends { [N in string]: unknown }>
extends FormComponentSuperclass<{ [N in string]: unknown }> extends ValidatedControlSuperclass<T>
implements OnChanges, Validator, OnInit implements OnChanges, OnInit
{ {
@Input() data: MetadataFormData<string, Field[]>; @Input() data: MetadataFormData<string, Field[]>;
control = this.fb.group<{ [N in string]: unknown }>({}); control = this.fb.group<T>({} as T);
labelControl = this.fb.control(false); labelControl = this.fb.control(false);
get hasLabel() {
return (
!!this.data.trueParent &&
this.data.trueParent.objectType !== 'union' &&
this.data.trueParent.typeGroup !== 'complex'
);
}
constructor(injector: Injector, private fb: FormBuilder) { constructor(injector: Injector, private fb: FormBuilder) {
super(injector); super(injector);
} }
@ -38,52 +45,46 @@ export class StructFormComponent
.subscribe(() => { .subscribe(() => {
this.emitOutgoingValue( this.emitOutgoingValue(
this.control.value && this.labelControl.value this.control.value && this.labelControl.value
? omitBy(this.control.value, isNil) ? (omitBy(this.control.value, isNil) as T)
: null : null
); );
}); });
return super.ngOnInit();
} }
ngOnChanges(changes: SimpleChanges) { ngOnChanges(changes: SimpleChanges) {
const newControlsNames = new Set(this.data.ast.map(({ name }) => name)); const newControlsNames = new Set(this.data.ast.map(({ name }) => name));
Object.keys(this.control.controls).forEach((name) => { Object.keys(this.control.controls).forEach((name) => {
if (newControlsNames.has(name)) newControlsNames.delete(name); if (newControlsNames.has(name)) newControlsNames.delete(name);
else this.control.removeControl(name); else this.control.removeControl(name as never);
}); });
newControlsNames.forEach((name) => newControlsNames.forEach((name) =>
this.control.addControl( this.control.addControl(
name, name as never,
this.fb.control(null, { this.fb.control(null, {
validators: validators:
this.data.ast.find((f) => f.name === name)?.option === 'required' this.data.ast.find((f) => f.name === name)?.option === 'required'
? [Validators.required] ? [Validators.required]
: [], : [],
}) }) as never
) )
); );
this.setLabelControl();
if (this.data.isRequired) {
this.labelControl.setValue(true);
this.labelControl.disable();
} else {
this.labelControl.setValue(false);
this.labelControl.enable();
}
super.ngOnChanges(changes); super.ngOnChanges(changes);
} }
handleIncomingValue(value: { [N in string]: unknown }) { handleIncomingValue(value: T) {
this.control.patchValue(value, { emitEvent: false }); this.control.patchValue(value as never, { emitEvent: false });
const newValue = this.labelControl.disabled || !!(value && Object.keys(value).length); this.setLabelControl(!!(value && Object.keys(value).length));
if (this.labelControl.value !== newValue) {
this.labelControl.setValue(newValue);
}
} }
validate(): ValidationErrors | null { private setLabelControl(value: boolean = false) {
return this.labelControl.value && this.control.invalid if (!this.hasLabel || this.data.isRequired) {
? this.control.errors || { structInvalid: true } if (!this.labelControl.value) this.labelControl.setValue(true);
: null; if (this.labelControl.enabled) this.labelControl.disable();
} else {
if (this.labelControl.value !== value) this.labelControl.setValue(value);
if (this.labelControl.disabled) this.labelControl.enable();
}
} }
} }

View File

@ -1,24 +1,15 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { ValidationErrors, Validator } from '@angular/forms';
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { TypeDefs } from '@vality/thrift-ts'; import { TypeDefs } from '@vality/thrift-ts';
import { createValidatedAbstractControlProviders } from '@cc/utils'; import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
@Component({ @Component({
selector: 'cc-typedef-form', selector: 'cc-typedef-form',
templateUrl: './typedef-form.component.html', templateUrl: './typedef-form.component.html',
providers: createValidatedAbstractControlProviders(TypedefFormComponent), providers: createControlProviders(TypedefFormComponent),
}) })
export class TypedefFormComponent export class TypedefFormComponent<T> extends ValidatedFormControlSuperclass<T> {
extends WrappedFormControlSuperclass<unknown>
implements Validator
{
@Input() data: MetadataFormData<string, TypeDefs[string]>; @Input() data: MetadataFormData<string, TypeDefs[string]>;
validate(): ValidationErrors | null {
return this.control.errors;
}
} }

View File

@ -7,7 +7,7 @@ import { Field } from '@vality/thrift-ts';
import { merge } from 'rxjs'; import { merge } from 'rxjs';
import { delay, distinctUntilChanged, map } from 'rxjs/operators'; import { delay, distinctUntilChanged, map } from 'rxjs/operators';
import { createValidatedAbstractControlProviders } from '@cc/utils'; import { createControlProviders, getErrorsTree } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data'; import { MetadataFormData } from '../../types/metadata-form-data';
import { getDefaultValue } from '../../utils/get-default-value'; import { getDefaultValue } from '../../utils/get-default-value';
@ -16,23 +16,23 @@ import { getDefaultValue } from '../../utils/get-default-value';
@Component({ @Component({
selector: 'cc-union-field', selector: 'cc-union-field',
templateUrl: './union-field.component.html', templateUrl: './union-field.component.html',
providers: createValidatedAbstractControlProviders(UnionFieldComponent), providers: createControlProviders(UnionFieldComponent),
}) })
export class UnionFieldComponent export class UnionFieldComponent<T extends { [N in string]: unknown }>
extends FormComponentSuperclass<{ [N in string]: unknown }> extends FormComponentSuperclass<T>
implements OnInit, Validator implements OnInit, Validator
{ {
@Input() data: MetadataFormData<string, Field[]>; @Input() data: MetadataFormData<string, Field[]>;
fieldControl = new FormControl<Field>(); fieldControl = new FormControl<Field>();
internalControl = new FormControl<unknown>(); internalControl = new FormControl<T[keyof T]>();
ngOnInit() { ngOnInit() {
merge(this.fieldControl.valueChanges, this.internalControl.valueChanges) merge(this.fieldControl.valueChanges, this.internalControl.valueChanges)
.pipe( .pipe(
map(() => { map(() => {
const field = this.fieldControl.value; const field = this.fieldControl.value;
return field ? { [field.name]: this.internalControl.value } : null; return field ? ({ [field.name]: this.internalControl.value } as T) : null;
}), }),
distinctUntilChanged(), distinctUntilChanged(),
delay(0), delay(0),
@ -44,14 +44,14 @@ export class UnionFieldComponent
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.fieldControl.invalid || this.internalControl.invalid return (
? { unionInvalid: true } (this.fieldControl.errors as ValidationErrors) || getErrorsTree(this.internalControl)
: null; );
} }
handleIncomingValue(value: { [N in string]: unknown }) { handleIncomingValue(value: T) {
if (value) { if (value) {
const name = Object.keys(value)[0]; const name: keyof T = Object.keys(value)[0];
this.fieldControl.setValue( this.fieldControl.setValue(
this.data.ast.find((f) => f.name === name), this.data.ast.find((f) => f.name === name),
{ emitEvent: false } { emitEvent: false }
@ -66,11 +66,11 @@ export class UnionFieldComponent
cleanInternal() { cleanInternal() {
this.internalControl.reset( this.internalControl.reset(
this.fieldControl.value 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])
: null, : null,
{ emitEvent: false } { emitEvent: false }
); );

View File

@ -1,7 +1,4 @@
<div <div [ngSwitch]="data?.typeGroup">
[ngSwitch]="data?.typeGroup"
[style]="data?.parent?.objectType === 'struct' ? 'padding-left: 16px' : ''"
>
<cc-primitive-field <cc-primitive-field
*ngSwitchCase="'primitive'" *ngSwitchCase="'primitive'"
[formControl]="control" [formControl]="control"

View File

@ -1,21 +1,20 @@
import { Component, Input, OnChanges } from '@angular/core'; import { Component, Input, OnChanges } from '@angular/core';
import { ValidationErrors, Validator } from '@angular/forms'; import { Validator } from '@angular/forms';
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { Field, ValueType } from '@vality/thrift-ts'; import { Field, ValueType } from '@vality/thrift-ts';
import { ThriftAstMetadata } from '@cc/app/api/utils'; import { ThriftAstMetadata } from '@cc/app/api/utils';
import { MetadataFormExtension } from '@cc/app/shared/components/metadata-form/types/metadata-form-extension'; import { MetadataFormExtension } from '@cc/app/shared/components/metadata-form/types/metadata-form-extension';
import { createValidatedAbstractControlProviders } from '@cc/utils'; import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from './types/metadata-form-data'; import { MetadataFormData } from './types/metadata-form-data';
@Component({ @Component({
selector: 'cc-metadata-form', selector: 'cc-metadata-form',
templateUrl: './metadata-form.component.html', templateUrl: './metadata-form.component.html',
providers: createValidatedAbstractControlProviders(MetadataFormComponent), providers: createControlProviders(MetadataFormComponent),
}) })
export class MetadataFormComponent export class MetadataFormComponent<T>
extends WrappedFormControlSuperclass<unknown> extends ValidatedFormControlSuperclass<T>
implements OnChanges, Validator implements OnChanges, Validator
{ {
@Input() metadata: ThriftAstMetadata[]; @Input() metadata: ThriftAstMetadata[];
@ -39,8 +38,4 @@ export class MetadataFormComponent
); );
} }
} }
validate(): ValidationErrors | null {
return this.control.errors;
}
} }

View File

@ -11,6 +11,7 @@ import { MatChipsModule } from '@angular/material/chips';
import { MatExpansionModule } from '@angular/material/expansion'; import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
@ -47,6 +48,7 @@ import { MetadataFormComponent } from './metadata-form.component';
ValueTypeTitleModule, ValueTypeTitleModule,
MatCheckboxModule, MatCheckboxModule,
MatChipsModule, MatChipsModule,
MatRadioModule,
], ],
declarations: [ declarations: [
MetadataFormComponent, MetadataFormComponent,

View File

@ -9,20 +9,17 @@ import { catchError, map, pluck, shareReplay, startWith } from 'rxjs/operators';
import { PartyManagementWithUserService } from '@cc/app/api/payment-processing'; import { PartyManagementWithUserService } from '@cc/app/api/payment-processing';
import { NotificationService } from '@cc/app/shared/services/notification'; import { NotificationService } from '@cc/app/shared/services/notification';
import { Option } from '@cc/components/select-search-field'; import { Option } from '@cc/components/select-search-field';
import { import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils/forms';
createValidatedAbstractControlProviders,
ValidatedWrappedAbstractControlSuperclass,
} from '@cc/utils/forms';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
selector: 'cc-payout-tool-field', selector: 'cc-payout-tool-field',
templateUrl: 'payout-tool-field.component.html', templateUrl: 'payout-tool-field.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
providers: createValidatedAbstractControlProviders(PayoutToolFieldComponent), providers: createControlProviders(PayoutToolFieldComponent),
}) })
export class PayoutToolFieldComponent export class PayoutToolFieldComponent
extends ValidatedWrappedAbstractControlSuperclass<PartyID> extends ValidatedControlSuperclass<PartyID>
implements OnInit implements OnInit
{ {
@Input() label: string; @Input() label: string;

View File

@ -15,10 +15,7 @@ import { filter, map, share, switchMap } from 'rxjs/operators';
import { PartyManagementWithUserService } from '@cc/app/api/payment-processing'; import { PartyManagementWithUserService } from '@cc/app/api/payment-processing';
import { ComponentChanges } from '@cc/app/shared/utils'; import { ComponentChanges } from '@cc/app/shared/utils';
import { import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils/forms';
createValidatedAbstractControlProviders,
ValidatedWrappedAbstractControlSuperclass,
} from '@cc/utils/forms';
import { RequiredSuper } from '@cc/utils/required-super'; import { RequiredSuper } from '@cc/utils/required-super';
@UntilDestroy() @UntilDestroy()
@ -26,11 +23,11 @@ import { RequiredSuper } from '@cc/utils/required-super';
selector: 'cc-shop-field', selector: 'cc-shop-field',
templateUrl: './shop-field.component.html', templateUrl: './shop-field.component.html',
styleUrls: ['./shop-field.component.scss'], styleUrls: ['./shop-field.component.scss'],
providers: createValidatedAbstractControlProviders(ShopFieldComponent), providers: createControlProviders(ShopFieldComponent),
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ShopFieldComponent<M extends boolean = boolean> export class ShopFieldComponent<M extends boolean = boolean>
extends ValidatedWrappedAbstractControlSuperclass< extends ValidatedControlSuperclass<
M extends true ? Shop[] : Shop, M extends true ? Shop[] : Shop,
M extends true ? ShopID[] : ShopID M extends true ? ShopID[] : ShopID
> >

View File

@ -0,0 +1,68 @@
import { Injectable } from '@angular/core';
import { DomainObject } from '@vality/domain-proto/lib/domain';
import { Field } from '@vality/thrift-ts';
import { from, Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { ThriftAstMetadata } from '@cc/app/api/utils';
import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service';
import { MetadataFormData, MetadataFormExtension } from '../../components/metadata-form';
import { createDomainObjectExtension } from './utils/create-domain-object-extension';
import {
defaultDomainObjectToOption,
DOMAIN_OBJECTS_TO_OPTIONS,
OtherDomainObjects,
} from './utils/domains-objects-to-options';
@Injectable({
providedIn: 'root',
})
export class DomainMetadataFormExtensionsService {
extensions$: Observable<MetadataFormExtension[]> = from(
import('@vality/domain-proto/lib/metadata.json').then(
(m) => m.default as never as ThriftAstMetadata[]
)
).pipe(
map((metadata) => this.createDomainObjectsOptions(metadata)),
shareReplay(1)
);
constructor(private domainStoreService: DomainStoreService) {}
private createDomainObjectsOptions(metadata: ThriftAstMetadata[]): MetadataFormExtension[] {
const domainFields = new MetadataFormData<string, Field[]>(
metadata,
'domain',
'DomainObject'
).ast;
return domainFields
.filter(
(f) => !(f.name in DOMAIN_OBJECTS_TO_OPTIONS) || DOMAIN_OBJECTS_TO_OPTIONS[f.name]
)
.map((f) =>
this.createFieldOptions(metadata, f.type as string, f.name as keyof DomainObject)
);
}
private createFieldOptions(
metadata: ThriftAstMetadata[],
objectType: string,
objectKey: keyof DomainObject
): MetadataFormExtension {
const objectFields = new MetadataFormData<string, Field[]>(metadata, 'domain', objectType)
.ast;
const refType = objectFields.find((n) => n.name === 'ref').type as string;
return createDomainObjectExtension(refType, () =>
this.domainStoreService.getObjects(objectKey).pipe(
map((objects) => {
const domainObjectToOption =
objectKey in DOMAIN_OBJECTS_TO_OPTIONS
? DOMAIN_OBJECTS_TO_OPTIONS[objectKey as keyof OtherDomainObjects]
: defaultDomainObjectToOption;
return objects.map(domainObjectToOption);
})
)
);
}
}

View File

@ -0,0 +1 @@
export * from './domain-metadata-form-extensions.service';

View File

@ -1,11 +1,15 @@
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { isTypeWithAliases, MetadataFormExtension } from '@cc/app/shared'; import {
isTypeWithAliases,
MetadataFormExtension,
MetadataFormExtensionOption,
} from '../../../components';
export function createDomainObjectMetadataFormExtension( export function createDomainObjectExtension(
refType: string, refType: string,
getObjects: () => Observable<{ ref: { id: number }; data: { name?: string } }[]> options: () => Observable<MetadataFormExtensionOption[]>
): MetadataFormExtension { ): MetadataFormExtension {
return { return {
determinant: (data) => determinant: (data) =>
@ -14,14 +18,17 @@ export function createDomainObjectMetadataFormExtension(
isTypeWithAliases(data, 'ObjectID', 'domain') isTypeWithAliases(data, 'ObjectID', 'domain')
), ),
extension: () => extension: () =>
getObjects().pipe( options().pipe(
map((objects) => ({ map((objects) => ({
options: objects options: objects
.sort((a, b) => a.ref.id - b.ref.id) .sort((a, b) =>
typeof a.value === 'number' && typeof b.value === 'number'
? a.value - b.value
: 0
)
.map((o) => ({ .map((o) => ({
label: o.data.name,
value: o.ref.id,
details: o, details: o,
...o,
})), })),
isIdentifier: true, isIdentifier: true,
})) }))

View File

@ -0,0 +1,31 @@
import { DomainObject } from '@vality/domain-proto';
import { PickByValue } from 'utility-types';
import { MetadataFormExtensionOption } from '../../../components';
type DomainRefDataObjects = PickByValue<
DomainObject,
{
ref: { id: number | string };
data: { name?: string; id?: string };
}
>;
export type OtherDomainObjects = Omit<DomainObject, keyof DomainRefDataObjects>;
export const DOMAIN_OBJECTS_TO_OPTIONS: {
[N in keyof OtherDomainObjects]-?: (o: OtherDomainObjects[N]) => MetadataFormExtensionOption;
} = {
/* eslint-disable @typescript-eslint/naming-convention */
currency: (o) => ({ value: o.ref.symbolic_code, label: o.data.name }),
payment_method: null,
globals: null,
identity_provider: (o) => ({ value: o.ref.id }),
dummy_link: (o) => ({ value: o.ref.id, label: o.data.link.id }),
/* eslint-enable @typescript-eslint/naming-convention */
};
export function defaultDomainObjectToOption(o: DomainRefDataObjects[keyof DomainRefDataObjects]) {
let label: string;
if ('name' in o.data) label = o.data.name;
if ('id' in o.data && !label) label = o.data.id;
return { value: o.ref.id, label };
}

View File

@ -1,8 +0,0 @@
import { NgModule } from '@angular/core';
import { ErrorService } from './error.service';
@NgModule({
providers: [ErrorService],
})
export class ErrorModule {}

View File

@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { NotificationService } from '../notification'; import { NotificationService } from '../notification';
// TODO: collect error information // TODO: collect error information
@Injectable() @Injectable({ providedIn: 'root' })
export class ErrorService { export class ErrorService {
constructor(private notificationService: NotificationService) {} constructor(private notificationService: NotificationService) {}

View File

@ -1,2 +1 @@
export * from './error.module';
export * from './error.service'; export * from './error.service';

View File

@ -6,3 +6,4 @@ export * from './user-info-based-id-generator';
export * from './partial-fetcher'; export * from './partial-fetcher';
export * from './query-params'; export * from './query-params';
export * from './moment-utc-date-adapter'; export * from './moment-utc-date-adapter';
export * from './domain-metadata-form-extensions';

View File

@ -16,8 +16,14 @@ export class BaseDialogService {
open<C, D, R, S>( open<C, D, R, S>(
dialogComponent: ComponentType<BaseDialogSuperclass<C, D, R, S>>, dialogComponent: ComponentType<BaseDialogSuperclass<C, D, R, S>>,
data: D = null, /**
configOrConfigName: Omit<MatDialogConfig<D>, 'data'> | keyof DialogConfig = {} * Workaround when both conditions for the 'data' argument must be true:
* - typing did not require passing when it is optional (for example: {param: number} | void)
* - typing required to pass when it is required (for example: {param: number})
*/
...[data, configOrConfigName]: D extends void
? []
: [data: D, configOrConfigName?: Omit<MatDialogConfig<D>, 'data'> | keyof DialogConfig]
): MatDialogRef<C, BaseDialogResponse<R, S>> { ): MatDialogRef<C, BaseDialogResponse<R, S>> {
return this.dialog.open(dialogComponent as never, { return this.dialog.open(dialogComponent as never, {
data, data,

View File

@ -1,4 +1,4 @@
<cc-base-dialog [title]="dialogData?.title || 'Confirm this action'" noContent> <cc-base-dialog [title]="title || 'Confirm this action'" noContent>
<cc-base-dialog-actions> <cc-base-dialog-actions>
<button mat-button (click)="cancel()">CANCEL</button> <button mat-button (click)="cancel()">CANCEL</button>
<button mat-raised-button color="primary" (click)="confirm()">CONFIRM</button> <button mat-raised-button color="primary" (click)="confirm()">CONFIRM</button>

View File

@ -9,8 +9,12 @@ import { BaseDialogResponseStatus, BaseDialogSuperclass } from '@cc/components/b
}) })
export class ConfirmActionDialogComponent extends BaseDialogSuperclass< export class ConfirmActionDialogComponent extends BaseDialogSuperclass<
ConfirmActionDialogComponent, ConfirmActionDialogComponent,
{ title?: string } { title?: string } | void
> { > {
get title() {
return typeof this.dialogData === 'object' ? this.dialogData.title : '';
}
cancel() { cancel() {
this.dialogRef.close({ status: BaseDialogResponseStatus.Cancelled }); this.dialogRef.close({ status: BaseDialogResponseStatus.Cancelled });
} }

View File

@ -1,25 +1,22 @@
import { AbstractControl } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { FormGroup, FormArray } from '@ngneat/reactive-forms';
import { ControlsValue } from '@ngneat/reactive-forms/lib/types'; import { ControlsValue } from '@ngneat/reactive-forms/lib/types';
function hasControls<T>(control: AbstractControl): control is FormGroup<T> | FormArray<T> { import { hasControls } from './has-controls';
return !!(control as any)?.controls;
}
export function getValue<T extends AbstractControl>(control: T): T['value'] { export function getValue<T extends AbstractControl>(control: T): T['value'] {
if (!hasControls(control)) { if (!hasControls(control)) {
return control.value; return control.value as never;
} }
if (Array.isArray(control.controls)) { if (Array.isArray(control.controls)) {
const result: ControlsValue<T>[] = []; const result: ControlsValue<T>[] = [];
for (const v of control.controls) { for (const v of control.controls) {
result.push(getValue(v as any)); result.push(getValue(v) as ControlsValue<T>);
} }
return result; return result;
} }
const result: Partial<ControlsValue<T>> = {}; const result: Partial<ControlsValue<T>> = {};
for (const [k, v] of Object.entries(control.controls)) { for (const [k, v] of Object.entries(control.controls)) {
result[k] = getValue(v as any); result[k] = getValue(v as AbstractControl) as ControlsValue<T>;
} }
return result; return result;
} }

View File

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

View File

@ -1,5 +1,5 @@
export * from './set-form-array-value'; export * from './set-form-array-value';
export * from './get-form-value-changes'; export * from './get-form-value-changes';
export * from './get-form-validation-changes'; export * from './get-form-validation-changes';
export * from './validated-wrapped-abstract-control-superclass'; export * from './validated-control-superclass';
export * from './switch-control'; export * from './switch-control';

View File

@ -4,6 +4,7 @@ import { provideValueAccessor } from '@s-libs/ng-core';
import { provideValidator } from './provide-validator'; import { provideValidator } from './provide-validator';
export const createValidatedAbstractControlProviders = ( export const createControlProviders = (component: ComponentType<unknown>): Provider[] => [
component: ComponentType<unknown> provideValueAccessor(component),
): Provider[] => [provideValueAccessor(component), provideValidator(component)]; provideValidator(component),
];

View File

@ -0,0 +1,4 @@
export * from './validated-control-superclass.directive';
export * from './provide-validator';
export * from './create-control-providers';
export { getErrorsTree } from '@cc/utils/forms/validated-control-superclass/utils/get-errors-tree';

View File

@ -0,0 +1,27 @@
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { hasControls } from '../../has-controls';
/**
* 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) => getErrorsTree(c));
} else {
errors.formGroupErrors = Object.fromEntries(
Array.from(Object.entries(control.controls)).map(([k, c]) => [
k,
getErrorsTree(c as AbstractControl),
])
);
}
}
return errors;
}

View File

@ -1,12 +1,14 @@
import { Directive, OnInit } from '@angular/core'; import { Directive, OnInit } from '@angular/core';
import { ValidationErrors, Validator } from '@angular/forms'; import { ValidationErrors, Validator } from '@angular/forms';
import { FormControl } from '@ngneat/reactive-forms';
import { WrappedControlSuperclass } from '@s-libs/ng-core'; import { WrappedControlSuperclass } from '@s-libs/ng-core';
import { RequiredSuper, REQUIRED_SUPER } from '../../required-super'; import { REQUIRED_SUPER, RequiredSuper } from '../../required-super';
import { getValue } from '../get-value'; import { getValue } from '../get-value';
import { getErrorsTree } from './utils/get-errors-tree';
@Directive() @Directive()
export abstract class ValidatedWrappedAbstractControlSuperclass<OuterType, InnerType = OuterType> export abstract class ValidatedControlSuperclass<OuterType, InnerType = OuterType>
extends WrappedControlSuperclass<OuterType, InnerType> extends WrappedControlSuperclass<OuterType, InnerType>
implements OnInit, Validator implements OnInit, Validator
{ {
@ -19,14 +21,21 @@ export abstract class ValidatedWrappedAbstractControlSuperclass<OuterType, Inner
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.control.errors; return getErrorsTree(this.control);
} }
protected outerToInner(outer: OuterType): InnerType { protected outerToInner(outer: OuterType): InnerType {
if (typeof this.emptyValue === 'object') { if (!outer && 'controls' in this.control) {
if (!outer) return this.emptyValue; return this.emptyValue;
return { ...this.emptyValue, ...outer };
} }
return outer as unknown as InnerType; return outer as never;
} }
} }
@Directive()
export class ValidatedFormControlSuperclass<
OuterType,
InnerType = OuterType
> extends ValidatedControlSuperclass<OuterType, InnerType> {
control = new FormControl<InnerType>();
}

View File

@ -1,3 +0,0 @@
export * from './validated-wrapped-abstract-control-superclass';
export * from './provide-validator';
export * from './create-validated-abstract-control-providers';