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 { SearchClaimsModule } from './sections/search-claims/search-claims.module';
import { SearchPartiesModule } from './sections/search-parties/search-parties.module';
import { SettingsModule } from './settings';
import { ThemeManager, ThemeManagerModule, ThemeName } from './theme-manager';
import {
DEFAULT_DIALOG_CONFIG,
@ -69,7 +68,6 @@ moment.locale('en');
DomainModule,
RepairingModule,
ThemeManagerModule,
SettingsModule,
PartyModule,
SearchPartiesModule,
SearchClaimsModule,

View File

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

View File

@ -3,19 +3,19 @@ import { ValidationErrors, Validator } from '@angular/forms';
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { Claim } from '@vality/domain-proto/lib/claim_management';
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 { 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';
@Component({
selector: 'cc-modification-form',
templateUrl: './modification-form.component.html',
providers: createValidatedAbstractControlProviders(ModificationFormComponent),
providers: createControlProviders(ModificationFormComponent),
})
export class ModificationFormComponent
extends WrappedFormControlSuperclass<unknown>
@ -26,37 +26,28 @@ export class ModificationFormComponent
@Input() type: string;
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);
}
ngOnChanges(changes: ComponentChanges<ModificationFormComponent>) {
super.ngOnChanges(changes);
if (changes.party || changes.claim) {
this.extensions = [
...createPartyClaimMetadataFormExtensions(this.party, this.claim),
...this.createDomainMetadataFormExtensions(),
];
this.extensions$ = this.domainMetadataFormExtensionsService.extensions$.pipe(
map((e) => [
...createPartyClaimMetadataFormExtensions(this.party, this.claim),
...e,
])
);
}
}
validate(): ValidationErrors | null {
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">
<div class="cc-headline">Change Delegate Ruleset</div>
<cc-base-dialog title="Change Delegate Ruleset">
<div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<mat-form-field>
<mat-label>Delegate Ruleset</mat-label>
@ -16,10 +14,9 @@
</mat-form-field>
</div>
<div fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="cancel()">CANCEL</button>
<cc-base-dialog-actions>
<button mat-button color="primary" (click)="changeRuleset()" [disabled]="form.invalid">
CHANGE RULESET
</button>
</div>
</div>
</cc-base-dialog-actions>
</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 { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { map } from 'rxjs/operators';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { RoutingRulesService } from '../../../thrift-services';
@UntilDestroy()
@ -12,7 +13,13 @@ import { RoutingRulesService } from '../../../thrift-services';
templateUrl: 'change-delegate-ruleset-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChangeDelegateRulesetDialogComponent implements OnInit {
export class ChangeDelegateRulesetDialogComponent
extends BaseDialogSuperclass<
ChangeDelegateRulesetDialogComponent,
{ mainRulesetRefID: number; delegateIdx: number }
>
implements OnInit
{
form = this.fb.group({
rulesetRefId: [],
description: '',
@ -21,17 +28,18 @@ export class ChangeDelegateRulesetDialogComponent implements OnInit {
rulesets$ = this.routingRulesService.rulesets$;
constructor(
injector: Injector,
private fb: FormBuilder,
private dialogRef: MatDialogRef<ChangeDelegateRulesetDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: { mainRulesetRefID: number; delegateIdx: number },
private routingRulesService: RoutingRulesService
) {}
) {
super(injector);
}
ngOnInit() {
this.routingRulesService
.getRuleset(this.data.mainRulesetRefID)
.getRuleset(this.dialogData.mainRulesetRefID)
.pipe(
map((r) => r?.data?.decisions?.delegates?.[this.data?.delegateIdx]),
map((r) => r?.data?.decisions?.delegates?.[this.dialogData?.delegateIdx]),
untilDestroyed(this)
)
.subscribe((delegate) => {
@ -42,15 +50,11 @@ export class ChangeDelegateRulesetDialogComponent implements OnInit {
});
}
cancel() {
this.dialogRef.close();
}
changeRuleset() {
this.routingRulesService
.changeDelegateRuleset({
mainRulesetRefID: this.data.mainRulesetRefID,
delegateIdx: this.data.delegateIdx,
mainRulesetRefID: this.dialogData.mainRulesetRefID,
delegateIdx: this.dialogData.delegateIdx,
newDelegateRulesetRefID: this.form.value.rulesetRefId,
description: this.form.value.description,
})

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
<form [formGroup]="form" fxLayout="column" fxLayoutGap="32px">
<div class="cc-headline">Attach party delegate ruleset</div>
<div fxLayout="column" fxLayoutGap="24px">
<cc-base-dialog title="Attach party delegate ruleset">
<div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<cc-target-ruleset-form
(valueChanges)="targetRuleset$.next($event)"
(valid)="targetRulesetValid$.next($event)"
@ -25,8 +23,7 @@
</ng-container>
</div>
<div fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="cancel()">CANCEL</button>
<cc-base-dialog-actions>
<button
mat-button
color="primary"
@ -35,5 +32,5 @@
>
ATTACH
</button>
</div>
</form>
</cc-base-dialog-actions>
</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 { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject } from 'rxjs';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { ErrorService } from '../../../../shared/services/error';
import { RoutingRulesService } from '../../../../thrift-services';
import { TargetRuleset } from '../../target-ruleset-form';
@ -13,7 +14,10 @@ import { TargetRuleset } from '../../target-ruleset-form';
templateUrl: 'attach-new-ruleset-dialog.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AttachNewRulesetDialogComponent {
export class AttachNewRulesetDialogComponent extends BaseDialogSuperclass<
AttachNewRulesetDialogComponent,
{ partyID: string }
> {
form = this.fb.group({
ruleset: this.fb.group({
name: 'submain ruleset[by shop id]',
@ -25,27 +29,27 @@ export class AttachNewRulesetDialogComponent {
targetRulesetValid$ = new BehaviorSubject<boolean>(undefined);
constructor(
injector: Injector,
private fb: FormBuilder,
private dialogRef: MatDialogRef<AttachNewRulesetDialogComponent>,
private paymentRoutingRulesService: RoutingRulesService,
@Inject(MAT_DIALOG_DATA) public data: { partyID: string },
private errorService: ErrorService
) {}
) {
super(injector);
}
attach() {
const { mainRulesetRefID, mainDelegateDescription } = this.targetRuleset$.value;
this.paymentRoutingRulesService
.attachPartyDelegateRuleset({
partyID: this.data.partyID,
partyID: this.dialogData.partyID,
mainRulesetRefID,
mainDelegateDescription,
ruleset: this.form.value.ruleset,
})
.pipe(untilDestroyed(this))
.subscribe(() => this.dialogRef.close(), this.errorService.error);
}
cancel() {
this.dialogRef.close();
.subscribe({
next: () => this.dialogRef.close(),
error: (err) => this.errorService.error(err),
});
}
}

View File

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

View File

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

View File

@ -1,11 +1,9 @@
<form [formGroup]="form" fxLayout="column" fxLayoutGap="32px">
<div class="cc-headline">Party payment routing rule params</div>
<div fxLayout="column" fxLayoutGap="24px">
<cc-base-dialog title="Party payment routing rule params">
<div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<mat-form-field>
<mat-label>Shop</mat-label>
<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 }}
</mat-option>
</mat-select>
@ -26,8 +24,7 @@
</div>
</div>
<div fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="cancel()">CANCEL</button>
<cc-base-dialog-actions>
<button mat-button color="primary" (click)="add()" [disabled]="form.invalid">ADD</button>
</div>
</form>
</cc-base-dialog-actions>
</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 { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Shop } from '@vality/domain-proto/lib/domain';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { ErrorService } from '../../../../shared/services/error';
import { RoutingRulesService } from '../../../../thrift-services';
@ -11,7 +12,10 @@ import { RoutingRulesService } from '../../../../thrift-services';
@Component({
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({
shopID: '',
name: 'Ruleset[candidates]',
@ -19,13 +23,13 @@ export class AddPartyPaymentRoutingRuleDialogComponent {
});
constructor(
injector: Injector,
private fb: FormBuilder,
private dialogRef: MatDialogRef<AddPartyPaymentRoutingRuleDialogComponent>,
private paymentRoutingRulesService: RoutingRulesService,
@Inject(MAT_DIALOG_DATA)
public data: { refID: number; partyID: string; shops: Shop[] },
private errorService: ErrorService
) {}
) {
super(injector);
}
add() {
const { shopID, name, description } = this.form.value;
@ -33,15 +37,11 @@ export class AddPartyPaymentRoutingRuleDialogComponent {
.addShopRuleset({
name,
description,
partyRulesetRefID: this.data.refID,
partyID: this.data.partyID,
partyRulesetRefID: this.dialogData.refID,
partyID: this.dialogData.partyID,
shopID,
})
.pipe(untilDestroyed(this))
.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 { 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';
@NgModule({
@ -29,7 +30,7 @@ import { AddPartyPaymentRoutingRuleDialogComponent } from './add-party-payment-r
MatSelectModule,
MatRadioModule,
MatAutocompleteModule,
ErrorModule,
BaseDialogModule,
],
declarations: [AddPartyPaymentRoutingRuleDialogComponent],
exports: [AddPartyPaymentRoutingRuleDialogComponent],

View File

@ -1,7 +1,5 @@
<form [formGroup]="form" fxLayout="column" fxLayoutGap="32px">
<div class="cc-headline">Payment rules init params</div>
<div fxLayout="column" fxLayoutGap="24px">
<cc-base-dialog title="Payment rules init params">
<div [formGroup]="form" fxLayout="column" fxLayoutGap="24px">
<mat-form-field>
<input
matInput
@ -25,8 +23,7 @@
</div>
</div>
<div fxLayout fxLayoutAlign="space-between">
<button mat-button (click)="cancel()">CANCEL</button>
<cc-base-dialog-actions>
<button mat-button color="primary" (click)="init()" [disabled]="form.invalid">INIT</button>
</div>
</form>
</cc-base-dialog-actions>
</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 { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BaseDialogSuperclass } from '@cc/components/base-dialog';
import { ErrorService } from '../../../../shared/services/error';
import { RoutingRulesService } from '../../../../thrift-services';
@ -11,7 +12,10 @@ import { RoutingRulesService } from '../../../../thrift-services';
selector: 'cc-initialize-payment-routing-rules-dialog',
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({
delegateDescription: 'Main delegate[party]',
name: 'submain ruleset[by shop id]',
@ -19,28 +23,25 @@ export class InitializePaymentRoutingRulesDialogComponent {
});
constructor(
injector: Injector,
private fb: FormBuilder,
private dialogRef: MatDialogRef<InitializePaymentRoutingRulesDialogComponent>,
private paymentRoutingRulesService: RoutingRulesService,
@Inject(MAT_DIALOG_DATA) public data: { partyID: string; refID: number },
private errorService: ErrorService
) {}
) {
super(injector);
}
init() {
const { delegateDescription, name, description } = this.form.value;
this.paymentRoutingRulesService
.addPartyRuleset({
name,
partyID: this.data.partyID,
mainRulesetRefID: this.data.refID,
partyID: this.dialogData.partyID,
mainRulesetRefID: this.dialogData.refID,
description,
delegateDescription,
})
.pipe(untilDestroyed(this))
.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 { 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';
@NgModule({
@ -29,7 +30,7 @@ import { InitializePaymentRoutingRulesDialogComponent } from './initialize-payme
MatSelectModule,
MatRadioModule,
MatAutocompleteModule,
ErrorModule,
BaseDialogModule,
],
declarations: [InitializePaymentRoutingRulesDialogComponent],
exports: [InitializePaymentRoutingRulesDialogComponent],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,9 @@ import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
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 { ExpanderComponent } from './expander';
import { PredicateComponent } from './predicate';
@ -30,6 +33,8 @@ import { PredicateComponent } from './predicate';
MatSelectModule,
MatRadioModule,
MatAutocompleteModule,
MetadataFormModule,
BaseDialogModule,
],
declarations: [AddShopPaymentRoutingRuleDialogComponent, PredicateComponent, ExpanderComponent],
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 { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatDialogRef } from '@angular/material/dialog';
import { Predicate } from '@vality/domain-proto/lib/domain';
import { of } from 'rxjs';
import { startWith, switchMap, take } from 'rxjs/operators';
import { BaseDialogResponseStatus } from '@cc/components/base-dialog';
import { RoutingRulesService, TerminalService } from '../../../../thrift-services';
import { AddShopPaymentRoutingRuleDialogComponent } from './add-shop-payment-routing-rule-dialog.component';
@ -37,8 +39,7 @@ export class AddShopPaymentRoutingRuleDialogService {
private fb: FormBuilder,
private dialogRef: MatDialogRef<AddShopPaymentRoutingRuleDialogComponent>,
private paymentRoutingRulesService: RoutingRulesService,
private terminalService: TerminalService,
@Inject(MAT_DIALOG_DATA) public data: { partyID: string; refID: number }
private terminalService: TerminalService
) {
this.form
.get('terminalType')
@ -62,7 +63,7 @@ export class AddShopPaymentRoutingRuleDialogService {
});
}
add(predicate: Predicate) {
add(predicate: Predicate, refID: number) {
const { description, weight, priority, terminalType, existentTerminalID, newTerminal } =
this.form.value;
(terminalType === TerminalType.New
@ -82,12 +83,12 @@ export class AddShopPaymentRoutingRuleDialogService {
weight,
priority,
terminalID,
refID: this.data.refID,
refID,
predicate,
})
)
)
.subscribe(() => this.dialogRef.close());
.subscribe(() => this.dialogRef.close({ status: BaseDialogResponseStatus.Success }));
}
addOption() {

View File

@ -1,203 +1,7 @@
<div fxLayout="column" fxLayoutGap="24px" [formGroup]="form">
<mat-radio-group formControlName="type" fxLayout="column" fxLayoutGap="16px">
<mat-radio-button [value]="predicateType.condition">Condition</mat-radio-button>
<div fxLayout>
<mat-radio-button [value]="predicateType.constant" fxFlex>Constant</mat-radio-button>
<mat-radio-button [value]="predicateType.allOf" fxFlex>All of</mat-radio-button>
</div>
<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>
<cc-metadata-form
[formControl]="control"
[metadata]="metadata$ | async"
[extensions]="extensions$ | async"
namespace="domain"
type="Predicate"
></cc-metadata-form>

View File

@ -1,302 +1,26 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import {
AbstractControl,
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 { Component, Injector, OnChanges } from '@angular/core';
import { Predicate } from '@vality/domain-proto/lib/domain';
import { from } from 'rxjs';
import { ComponentChanges } from '@cc/app/shared/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 */
import { DomainMetadataFormExtensionsService } from '@cc/app/shared/services';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
@Component({
selector: 'cc-predicate',
templateUrl: 'predicate.component.html',
styleUrls: ['predicate.component.scss'],
providers: createControlProviders(PredicateComponent),
})
export class PredicateComponent implements OnChanges {
@Input() form = this.createForm();
@Output() validationChange = new EventEmitter<boolean>();
@Output() predicateChange = new EventEmitter<Predicate>();
export class PredicateComponent
extends ValidatedFormControlSuperclass<Predicate>
implements OnChanges
{
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions$ = this.domainMetadataFormExtensionsService.extensions$;
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:
TokenizationMethod[value.tokenizationMethod as string],
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) {
return control.valueChanges.pipe(
startWith(control.value),
map((v) => v.trim().toLowerCase()),
map((v) =>
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 } };
constructor(
injector: Injector,
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService
) {
super(injector);
}
}

View File

@ -1,21 +1,19 @@
import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Predicate, TerminalObject } from '@vality/domain-proto/lib/domain';
import { combineLatest } from 'rxjs';
import { map, shareReplay, switchMap, take } from 'rxjs/operators';
import { first, map, shareReplay, switchMap } from 'rxjs/operators';
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 { damselInstanceToObject } from '../../../thrift-services';
import { DomainStoreService } from '../../../thrift-services/damsel/domain-store.service';
import { AddShopPaymentRoutingRuleDialogComponent } from './add-shop-payment-routing-rule-dialog';
import { ShopPaymentRoutingRulesetService } from './shop-payment-routing-ruleset.service';
const DIALOG_WIDTH = '548px';
@UntilDestroy()
@Component({
selector: 'cc-shop-payment-routing-ruleset',
@ -44,30 +42,40 @@ export class ShopPaymentRoutingRulesetComponent {
isLoading$ = this.domainStoreService.isLoading$;
constructor(
private dialog: MatDialog,
private baseDialogService: BaseDialogService,
private shopPaymentRoutingRulesetService: ShopPaymentRoutingRulesetService,
private domainStoreService: DomainStoreService,
private errorService: ErrorService
private errorService: ErrorService,
private notificationService: NotificationService
) {}
addShopRule() {
combineLatest([this.partyID$, this.shopPaymentRoutingRulesetService.refID$])
this.shopPaymentRoutingRulesetService.refID$
.pipe(
take(1),
switchMap(([partyID, refID]) =>
this.dialog
.open(AddShopPaymentRoutingRuleDialogComponent, {
disableClose: true,
width: DIALOG_WIDTH,
maxHeight: '90vh',
data: { partyID, refID },
})
first(),
switchMap((refID) =>
this.baseDialogService
.open(AddShopPaymentRoutingRuleDialogComponent, { refID })
.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) {

View File

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

View File

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

View File

@ -6,10 +6,7 @@ import { Party, Shop } from '@vality/magista-proto/lib/domain';
import { Moment } from 'moment';
import * as moment from 'moment';
import {
createValidatedAbstractControlProviders,
ValidatedWrappedAbstractControlSuperclass,
} from '@cc/utils/forms';
import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils/forms';
import { getEnumKeys } from '@cc/utils/get-enum-keys';
export interface PayoutsSearchForm {
@ -25,9 +22,9 @@ export interface PayoutsSearchForm {
@Component({
selector: 'cc-payouts-search-form',
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>({
payoutId: 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">
<span class="cc-body-1">
<span class="cc-body-1" *ngIf="hasLabel">
<cc-field-label [field]="data.field" [type]="data.type"></cc-field-label>
({{ data.type.name | titlecase }})
</span>
<mat-accordion>
<mat-accordion [ngStyle]="{ 'padding-left': hasLabel && '16px' }">
<mat-expansion-panel
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-panel-title fxLayoutAlign=" center">
{{ i + 1 }}.
{{
data.type.name === 'map'
? (data.type.keyType | valueTypeTitle | titlecase) + ' - '
: ''
}}
{{ hasKeys ? (keyType | valueTypeTitle | titlecase) + ' - ' : '' }}
{{ data.type.valueType | valueTypeTitle | titlecase }}
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end">
@ -25,18 +21,20 @@
</mat-panel-description>
</mat-expansion-panel-header>
<div gdColumns="1fr" gdGap="16px">
<ng-container *ngIf="data.type.name === 'map'">
<ng-container *ngIf="hasKeys">
<span class="cc-body-2">Key</span>
<cc-metadata-form
[formControl]="keyControls.controls[i]"
[metadata]="data.metadata"
[namespace]="data.namespace"
[type]="data.type.keyType"
[type]="keyType"
[parent]="data"
[extensions]="data.extensions"
></cc-metadata-form>
</ng-container>
<span class="cc-body-2" *ngIf="data.type.name === 'map'">Value</span>
<cc-metadata-form
[formControl]="valueControl"
[metadata]="data.metadata"
[namespace]="data.namespace"
[type]="data.type.valueType"

View File

@ -1,5 +1,5 @@
mat-expansion-panel-body {
padding: 0;
::ng-deep .mat-expansion-panel-body {
padding: 0 !important;
}
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 { 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 { createValidatedAbstractControlProviders } from '@cc/utils';
import { createControlProviders, getErrorsTree } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data';
@UntilDestroy()
@Component({
selector: 'cc-complex-form',
templateUrl: './complex-form.component.html',
styleUrls: ['complex-form.component.scss'],
providers: createValidatedAbstractControlProviders(ComplexFormComponent),
providers: createControlProviders(ComplexFormComponent),
})
export class ComplexFormComponent
extends WrappedFormControlSuperclass<unknown>
implements Validator
export class ComplexFormComponent<T extends unknown[] | Map<unknown, unknown> | Set<unknown>>
extends FormComponentSuperclass<T>
implements OnInit, Validator
{
@Input() data: MetadataFormData<SetType | MapType | ListType>;
controls = new FormArray([]);
valueControls = new FormArray([]);
keyControls = new FormArray([]);
add() {
this.controls.push(new FormControl());
get hasLabel() {
return !!this.data.trueParent;
}
delete(idx: number) {
this.controls.removeAt(idx);
get hasKeys() {
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 {
return this.control.invalid || this.controls.invalid
? { [this.data.type.name + 'Invalid']: true }
: null;
return getErrorsTree(this.keyControls) || getErrorsTree(this.valueControls);
}
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>
</mat-label>
<mat-select [formControl]="control" [required]="data.isRequired">
<mat-option *ngFor="let item of data.ast.items" [value]="item.value">{{
item.name
}}</mat-option>
<mat-option
*ngFor="let item of data.ast.items; let idx = index"
[value]="item.value ?? idx"
>
{{ item.name }}
</mat-option>
</mat-select>
<button
matSuffix

View File

@ -1,21 +1,15 @@
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 { createValidatedAbstractControlProviders } from '@cc/utils';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data';
@Component({
selector: 'cc-enum-field',
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]>;
validate(): ValidationErrors | null {
return this.control.errors;
}
}

View File

@ -1,54 +1,87 @@
<div gdColumns="1fr" gdGap="16px">
<div fxLayoutGap="4px">
<mat-form-field fxFlex>
<mat-label>
<cc-field-label [field]="data.field" [type]="data.type"></cc-field-label>
</mat-label>
<input
matInput
[matAutocomplete]="auto"
#trigger="matAutocompleteTrigger"
[formControl]="control"
[required]="data.isRequired"
[type]="inputType"
[ngClass]="{ 'cc-code': (data.extensionResult$ | async)?.isIdentifier }"
/>
<div matSuffix fxLayoutGap="4px">
<button mat-icon-button *ngIf="control.value" (click)="clear($event)">
<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="(extensionResult$ | async)?.options?.length"
mat-icon-button
(click)="
auto.isOpen ? trigger.closePanel() : trigger.openPanel();
$event.stopPropagation()
"
>
<mat-icon>{{ auto.isOpen ? 'expand_less' : 'expand_more' }}</mat-icon>
<button *ngIf="generate$ | async" mat-icon-button (click)="generate($event)">
<mat-icon>loop</mat-icon>
</button>
</div>
<mat-hint>{{ aliases }}</mat-hint>
<mat-autocomplete #auto="matAutocomplete">
<ng-container *ngIf="data.extensionResult$ | async as extensionResult">
<mat-option
*ngFor="let option of filteredOptions$ | async"
[value]="option.value"
</div>
</ng-container>
<ng-template #input>
<div fxLayoutGap="4px">
<mat-form-field fxFlex>
<mat-label>
<cc-field-label [field]="data.field" [type]="data.type"></cc-field-label>
</mat-label>
<input
matInput
[matAutocomplete]="auto"
#trigger="matAutocompleteTrigger"
[formControl]="control"
[required]="data.isRequired"
[type]="inputType"
[ngClass]="{ 'cc-code': (data.extensionResult$ | async)?.isIdentifier }"
/>
<div matSuffix fxLayoutGap="4px">
<button
mat-icon-button
*ngIf="!data.isRequired && control.value"
(click)="clear($event)"
>
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign=" center">
<div [ngClass]="{ 'cc-code': extensionResult.isIdentifier }">
{{ option.value }}
<mat-icon>clear</mat-icon>
</button>
<button
*ngIf="(extensionResult$ | async)?.options?.length"
mat-icon-button
(click)="
auto.isOpen ? trigger.closePanel() : trigger.openPanel();
$event.stopPropagation()
"
>
<mat-icon>{{ auto.isOpen ? 'expand_less' : 'expand_more' }}</mat-icon>
</button>
</div>
<mat-hint>{{ aliases }}</mat-hint>
<mat-autocomplete #auto="matAutocomplete">
<ng-container *ngIf="data.extensionResult$ | async as extensionResult">
<mat-option
*ngFor="let option of filteredOptions$ | async"
[value]="option.value"
>
<div fxLayout="row" fxLayoutGap="8px" fxLayoutAlign=" center">
<div [ngClass]="{ 'cc-code': extensionResult.isIdentifier }">
{{ option.value }}
</div>
<cc-label [label]="option.label" [color]="option.color"></cc-label>
</div>
<cc-label [label]="option.label" [color]="option.color"></cc-label>
</div>
</mat-option>
</ng-container>
</mat-autocomplete>
</mat-form-field>
<button *ngIf="generate$ | async" mat-icon-button (click)="generate($event)">
<mat-icon>loop</mat-icon>
</button>
</div>
</mat-option>
</ng-container>
</mat-autocomplete>
</mat-form-field>
<button *ngIf="generate$ | async" mat-icon-button (click)="generate($event)">
<mat-icon>loop</mat-icon>
</button>
</div>
</ng-template>
<ng-container *ngIf="selected$ | async as selected">
<mat-expansion-panel *ngIf="selected.details">
<mat-expansion-panel-header>

View File

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

View File

@ -1,5 +1,5 @@
<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">
<ng-container [ngTemplateOutlet]="label"></ng-container>
</mat-checkbox>
@ -14,6 +14,7 @@
<ng-container *ngIf="labelControl.value">
<cc-metadata-form
*ngFor="let field of data.ast"
[ngStyle]="{ 'padding-left': hasLabel && '16px' }"
[formControl]="control.get(field.name)"
[metadata]="data.metadata"
[namespace]="data.namespace"

View File

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

View File

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

View File

@ -1,21 +1,20 @@
import { Component, Input, OnChanges } from '@angular/core';
import { ValidationErrors, Validator } from '@angular/forms';
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
import { Validator } from '@angular/forms';
import { Field, ValueType } from '@vality/thrift-ts';
import { ThriftAstMetadata } from '@cc/app/api/utils';
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';
@Component({
selector: 'cc-metadata-form',
templateUrl: './metadata-form.component.html',
providers: createValidatedAbstractControlProviders(MetadataFormComponent),
providers: createControlProviders(MetadataFormComponent),
})
export class MetadataFormComponent
extends WrappedFormControlSuperclass<unknown>
export class MetadataFormComponent<T>
extends ValidatedFormControlSuperclass<T>
implements OnChanges, Validator
{
@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 { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
@ -47,6 +48,7 @@ import { MetadataFormComponent } from './metadata-form.component';
ValueTypeTitleModule,
MatCheckboxModule,
MatChipsModule,
MatRadioModule,
],
declarations: [
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 { NotificationService } from '@cc/app/shared/services/notification';
import { Option } from '@cc/components/select-search-field';
import {
createValidatedAbstractControlProviders,
ValidatedWrappedAbstractControlSuperclass,
} from '@cc/utils/forms';
import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils/forms';
@UntilDestroy()
@Component({
selector: 'cc-payout-tool-field',
templateUrl: 'payout-tool-field.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: createValidatedAbstractControlProviders(PayoutToolFieldComponent),
providers: createControlProviders(PayoutToolFieldComponent),
})
export class PayoutToolFieldComponent
extends ValidatedWrappedAbstractControlSuperclass<PartyID>
extends ValidatedControlSuperclass<PartyID>
implements OnInit
{
@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 { ComponentChanges } from '@cc/app/shared/utils';
import {
createValidatedAbstractControlProviders,
ValidatedWrappedAbstractControlSuperclass,
} from '@cc/utils/forms';
import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils/forms';
import { RequiredSuper } from '@cc/utils/required-super';
@UntilDestroy()
@ -26,11 +23,11 @@ import { RequiredSuper } from '@cc/utils/required-super';
selector: 'cc-shop-field',
templateUrl: './shop-field.component.html',
styleUrls: ['./shop-field.component.scss'],
providers: createValidatedAbstractControlProviders(ShopFieldComponent),
providers: createControlProviders(ShopFieldComponent),
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopFieldComponent<M extends boolean = boolean>
extends ValidatedWrappedAbstractControlSuperclass<
extends ValidatedControlSuperclass<
M extends true ? Shop[] : Shop,
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 { 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,
getObjects: () => Observable<{ ref: { id: number }; data: { name?: string } }[]>
options: () => Observable<MetadataFormExtensionOption[]>
): MetadataFormExtension {
return {
determinant: (data) =>
@ -14,14 +18,17 @@ export function createDomainObjectMetadataFormExtension(
isTypeWithAliases(data, 'ObjectID', 'domain')
),
extension: () =>
getObjects().pipe(
options().pipe(
map((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) => ({
label: o.data.name,
value: o.ref.id,
details: o,
...o,
})),
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';
// TODO: collect error information
@Injectable()
@Injectable({ providedIn: 'root' })
export class ErrorService {
constructor(private notificationService: NotificationService) {}

View File

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

View File

@ -6,3 +6,4 @@ export * from './user-info-based-id-generator';
export * from './partial-fetcher';
export * from './query-params';
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>(
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>> {
return this.dialog.open(dialogComponent as never, {
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>
<button mat-button (click)="cancel()">CANCEL</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<
ConfirmActionDialogComponent,
{ title?: string }
{ title?: string } | void
> {
get title() {
return typeof this.dialogData === 'object' ? this.dialogData.title : '';
}
cancel() {
this.dialogRef.close({ status: BaseDialogResponseStatus.Cancelled });
}

View File

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

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 './get-form-value-changes';
export * from './get-form-validation-changes';
export * from './validated-wrapped-abstract-control-superclass';
export * from './validated-control-superclass';
export * from './switch-control';

View File

@ -4,6 +4,7 @@ import { provideValueAccessor } from '@s-libs/ng-core';
import { provideValidator } from './provide-validator';
export const createValidatedAbstractControlProviders = (
component: ComponentType<unknown>
): Provider[] => [provideValueAccessor(component), provideValidator(component)];
export const createControlProviders = (component: ComponentType<unknown>): Provider[] => [
provideValueAccessor(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 { ValidationErrors, Validator } from '@angular/forms';
import { FormControl } from '@ngneat/reactive-forms';
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 { getErrorsTree } from './utils/get-errors-tree';
@Directive()
export abstract class ValidatedWrappedAbstractControlSuperclass<OuterType, InnerType = OuterType>
export abstract class ValidatedControlSuperclass<OuterType, InnerType = OuterType>
extends WrappedControlSuperclass<OuterType, InnerType>
implements OnInit, Validator
{
@ -19,14 +21,21 @@ export abstract class ValidatedWrappedAbstractControlSuperclass<OuterType, Inner
}
validate(): ValidationErrors | null {
return this.control.errors;
return getErrorsTree(this.control);
}
protected outerToInner(outer: OuterType): InnerType {
if (typeof this.emptyValue === 'object') {
if (!outer) return this.emptyValue;
return { ...this.emptyValue, ...outer };
if (!outer && 'controls' in this.control) {
return this.emptyValue;
}
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';