TD-869: New deposit details page. New create revert dialog (#334)

This commit is contained in:
Rinat Arsaev 2024-02-22 17:54:59 +07:00 committed by GitHub
parent 7e5ac50b8f
commit 1b060558bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 490 additions and 344 deletions

8
package-lock.json generated
View File

@ -25,7 +25,7 @@
"@vality/fistful-proto": "2.0.1-8ecf2b7.0", "@vality/fistful-proto": "2.0.1-8ecf2b7.0",
"@vality/machinegun-proto": "1.0.0", "@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0", "@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/ng-core": "^17.2.1-pr-57-0134d85.0", "@vality/ng-core": "^17.2.1-pr-57-3adeb57.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0", "@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0", "@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0", "@vality/thrift-ts": "2.4.1-8ad5123.0",
@ -6454,9 +6454,9 @@
"integrity": "sha512-BsDy5ejotfTtUlwuoX3kz+PYJ5NSTW6m5ZRGv+p5HaKXSjR7tserPdv0q133Wp4T+sg0ED0Qr9Peqsrn+9XlDQ==" "integrity": "sha512-BsDy5ejotfTtUlwuoX3kz+PYJ5NSTW6m5ZRGv+p5HaKXSjR7tserPdv0q133Wp4T+sg0ED0Qr9Peqsrn+9XlDQ=="
}, },
"node_modules/@vality/ng-core": { "node_modules/@vality/ng-core": {
"version": "17.2.1-pr-57-0134d85.0", "version": "17.2.1-pr-57-3adeb57.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.2.1-pr-57-0134d85.0.tgz", "resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.2.1-pr-57-3adeb57.0.tgz",
"integrity": "sha512-0wlLzf2+smFYVYqu/iEptyeKrasRsxWRd2hNwqG+cd0eiIwxNWyBMaI0+FuWS141CtodSgF8Ab8CAG2ceD+vkw==", "integrity": "sha512-h8/U6pUrJDMTXUj2HL9xC6KjlVa1kNj20DcI79gaoYmh//4L/U+y6vF75ieOeZW8MUFDD9WcVGfrwsRFWOntqQ==",
"dependencies": { "dependencies": {
"@angular/material-date-fns-adapter": "^17.2.0", "@angular/material-date-fns-adapter": "^17.2.0",
"@ng-matero/extensions": "^17.1.0", "@ng-matero/extensions": "^17.1.0",

View File

@ -33,7 +33,7 @@
"@vality/fistful-proto": "2.0.1-8ecf2b7.0", "@vality/fistful-proto": "2.0.1-8ecf2b7.0",
"@vality/machinegun-proto": "1.0.0", "@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0", "@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/ng-core": "^17.2.1-pr-57-0134d85.0", "@vality/ng-core": "^17.2.1-pr-57-3adeb57.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0", "@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0", "@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0", "@vality/thrift-ts": "2.4.1-8ad5123.0",

View File

@ -1,7 +1,14 @@
<cc-page-layout title="Deposit details"> <cc-page-layout [progress]="isLoading$ | async" title="Deposit details">
<cc-deposit-main-info <mat-card *ngIf="deposit$ | async">
*ngIf="deposit$ | async as deposit" <mat-card-content>
[deposit]="deposit" <cc-thrift-viewer
></cc-deposit-main-info> [extensions]="extensions$ | async"
[metadata]="metadata$ | async"
[value]="deposit$ | async"
namespace="fistful_stat"
type="StatDeposit"
></cc-thrift-viewer>
</mat-card-content>
</mat-card>
<cc-reverts *ngIf="deposit$ | async as deposit" [deposit]="deposit"></cc-reverts> <cc-reverts *ngIf="deposit$ | async as deposit" [deposit]="deposit"></cc-reverts>
</cc-page-layout> </cc-page-layout>

View File

@ -1,6 +1,22 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { formatDate } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnInit, Inject, LOCALE_ID } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { pluck, take } from 'rxjs/operators'; import { DepositStatus, RevertStatus } from '@vality/fistful-proto/fistful_stat';
import { Timestamp } from '@vality/fistful-proto/internal/base';
import { formatCurrency, getImportValue } from '@vality/ng-core';
import startCase from 'lodash-es/startCase';
import { of, Observable } from 'rxjs';
import { take, map } from 'rxjs/operators';
import { getUnionKey, getUnionValue } from '../../../utils';
import { ManagementService } from '../../api/wallet';
import {
MetadataViewExtension,
MetadataViewExtensionResult,
} from '../../shared/components/json-viewer';
import { isTypeWithAliases } from '../../shared/components/metadata-form';
import { AmountCurrencyService } from '../../shared/services';
import { FetchSourcesService } from '../sources';
import { ReceiveDepositService } from './services/receive-deposit/receive-deposit.service'; import { ReceiveDepositService } from './services/receive-deposit/receive-deposit.service';
@ -11,15 +27,114 @@ import { ReceiveDepositService } from './services/receive-deposit/receive-deposi
}) })
export class DepositDetailsComponent implements OnInit { export class DepositDetailsComponent implements OnInit {
deposit$ = this.fetchDepositService.deposit$; deposit$ = this.fetchDepositService.deposit$;
isLoading$ = this.fetchDepositService.isLoading$;
metadata$ = getImportValue(import('@vality/fistful-proto/metadata.json'));
extensions$: Observable<MetadataViewExtension[]> = this.fetchDepositService.deposit$.pipe(
map((deposit) => [
{
determinant: (d) => of(isTypeWithAliases(d, 'Timestamp', 'base')),
extension: (_, value: Timestamp) =>
of({ value: formatDate(value, 'dd.MM.yyyy HH:mm:ss', 'en') }),
},
{
determinant: (d) =>
of(isTypeWithAliases(d, 'CurrencySymbolicCode', 'fistful_stat')),
extension: () => of({ hidden: true }),
},
{
determinant: (d) => of(isTypeWithAliases(d, 'Amount', 'base')),
extension: (_, amount: number) =>
this.amountCurrencyService.getCurrency(deposit.currency_symbolic_code).pipe(
map((c) => ({
value: formatCurrency(
amount,
c.data.symbolic_code,
'long',
this._locale,
c.data.exponent,
),
})),
),
},
{
determinant: (d) => of(isTypeWithAliases(d, 'DepositStatus', 'fistful_stat')),
extension: (_, status: DepositStatus) =>
of({
value: startCase(getUnionKey(status)),
tooltip: Object.keys(getUnionValue(status)).length
? getUnionValue(status)
: undefined,
tag: true,
color: (
{
failed: 'warn',
pending: 'pending',
succeeded: 'success',
} as const
)[getUnionKey(status)],
}),
},
{
determinant: (d) => of(isTypeWithAliases(d, 'RevertStatus', 'fistful_stat')),
extension: (_, status: RevertStatus, viewValue: string) =>
of({
value: startCase(viewValue),
tag: true,
color: (
{
[1]: 'pending',
[2]: 'success',
} as const
)[status],
}),
},
{
determinant: (d) => of(isTypeWithAliases(d, 'WalletID', 'fistful_stat')),
extension: (_, id: string) =>
this.walletManagementService.Get(id, {}).pipe(
map(
(wallet): MetadataViewExtensionResult => ({
value: wallet.name,
tooltip: wallet.id,
link: [
['/wallets'],
{ queryParams: { wallet_id: JSON.stringify([wallet.id]) } },
],
}),
),
),
},
{
determinant: (d) => of(isTypeWithAliases(d, 'SourceID', 'fistful_stat')),
extension: (_, id: string) =>
this.fetchSourcesService.sources$.pipe(
map((sources) => sources.find((s) => s.id === id)),
map(
(source): MetadataViewExtensionResult => ({
value: source.name,
tooltip: source.id,
}),
),
),
},
]),
);
constructor( constructor(
private fetchDepositService: ReceiveDepositService, private fetchDepositService: ReceiveDepositService,
private route: ActivatedRoute, private route: ActivatedRoute,
@Inject(LOCALE_ID) private _locale: string,
private amountCurrencyService: AmountCurrencyService,
private walletManagementService: ManagementService,
private fetchSourcesService: FetchSourcesService,
) {} ) {}
ngOnInit() { ngOnInit() {
this.route.params this.route.params
.pipe(take(1), pluck('depositID')) .pipe(
take(1),
map((p) => p.depositID),
)
.subscribe((depositID) => this.fetchDepositService.receiveDeposit(depositID)); .subscribe((depositID) => this.fetchDepositService.receiveDeposit(depositID));
} }
} }

View File

@ -9,9 +9,10 @@ import { StatusModule, PageLayoutModule } from '@cc/app/shared/components';
import { DetailsItemModule } from '@cc/components/details-item'; import { DetailsItemModule } from '@cc/components/details-item';
import { HeadlineModule } from '@cc/components/headline'; import { HeadlineModule } from '@cc/components/headline';
import { ThriftViewerModule } from '../../shared/components/thrift-viewer';
import { DepositDetailsRoutingModule } from './deposit-details-routing.module'; import { DepositDetailsRoutingModule } from './deposit-details-routing.module';
import { DepositDetailsComponent } from './deposit-details.component'; import { DepositDetailsComponent } from './deposit-details.component';
import { DepositMainInfoModule } from './deposit-main-info/deposit-main-info.module';
import { RevertsModule } from './reverts/reverts.module'; import { RevertsModule } from './reverts/reverts.module';
@NgModule({ @NgModule({
@ -25,9 +26,9 @@ import { RevertsModule } from './reverts/reverts.module';
MatProgressSpinnerModule, MatProgressSpinnerModule,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,
DepositMainInfoModule,
RevertsModule, RevertsModule,
PageLayoutModule, PageLayoutModule,
ThriftViewerModule,
], ],
declarations: [DepositDetailsComponent], declarations: [DepositDetailsComponent],
}) })

View File

@ -1,26 +0,0 @@
<mat-card>
<mat-card-content style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px">
<cc-details-item style="grid-column: span 2" title="ID">{{ deposit.id }}</cc-details-item>
<cc-details-item title="Status">
<cc-status [color]="deposit.status | toStatus | toDepositColor">{{
deposit.status | toStatus
}}</cc-status>
</cc-details-item>
<cc-details-item title="Amount"
>{{ deposit.amount | ccThriftInt64 | ccFormatAmount }}
{{ deposit.currency_symbolic_code | ccCurrency }}</cc-details-item
>
<cc-details-item title="Fee"
>{{ deposit.fee | ccThriftInt64 | ccFormatAmount }}
{{ deposit.currency_symbolic_code | ccCurrency }}</cc-details-item
>
<cc-details-item title="Created At">{{
deposit.created_at | date: 'dd.MM.yyyy HH:mm:ss'
}}</cc-details-item>
<cc-details-item title="Source ID">{{ deposit.source_id }}</cc-details-item>
<cc-details-item title="Identity ID">{{ deposit.identity_id }}</cc-details-item>
<cc-details-item title="Destination">
<cc-wallet-info [walletID]="deposit.destination_id"></cc-wallet-info>
</cc-details-item>
</mat-card-content>
</mat-card>

View File

@ -1,12 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { StatDeposit } from '@vality/fistful-proto/fistful_stat';
@Component({
selector: 'cc-deposit-main-info',
templateUrl: 'deposit-main-info.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DepositMainInfoComponent {
@Input()
deposit: StatDeposit;
}

View File

@ -1,25 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { StatusModule } from '@cc/app/shared/components';
import { WalletInfoModule } from '@cc/app/shared/components/wallet-info';
import { CommonPipesModule, ThriftPipesModule } from '@cc/app/shared/pipes';
import { DetailsItemModule } from '@cc/components/details-item';
import { DepositMainInfoComponent } from './deposit-main-info.component';
@NgModule({
imports: [
CommonModule,
MatCardModule,
DetailsItemModule,
StatusModule,
ThriftPipesModule,
CommonPipesModule,
WalletInfoModule,
],
declarations: [DepositMainInfoComponent],
exports: [DepositMainInfoComponent],
})
export class DepositMainInfoModule {}

View File

@ -1,44 +1,15 @@
<v-dialog [progress]="!!(progress$ | async)" title="Create revert"> <v-dialog [progress]="!!(progress$ | async)" title="Create revert">
<div *ngIf="form" [formGroup]="form" style="display: flex; flex-direction: column; gap: 16px"> <cc-fistful-thrift-form
<div style="display: flex; gap: 24px"> [extensions]="extensions"
<mat-form-field style="flex: 1"> [formControl]="control"
<input namespace="deposit_revert"
formControlName="amount" noChangeKind
matInput type="RevertParams"
placeholder="Amount" ></cc-fistful-thrift-form>
required
type="number"
/>
</mat-form-field>
<mat-form-field style="flex: 1">
<input
formControlName="currency"
matInput
placeholder="Currency"
readonly
required
type="text"
/>
</mat-form-field>
</div>
<div style="display: flex; gap: 24px">
<mat-form-field style="flex: 1">
<input formControlName="reason" matInput placeholder="Reason" type="text" />
</mat-form-field>
<mat-form-field style="flex: 1">
<input
formControlName="externalID"
matInput
placeholder="External ID"
type="text"
/>
</mat-form-field>
</div>
</div>
<v-dialog-actions> <v-dialog-actions>
<button <button
[disabled]="!!(progress$ | async) || form.invalid" [disabled]="!!(progress$ | async) || control.invalid"
color="primary" color="primary"
mat-button mat-button
(click)="createRevert()" (click)="createRevert()"
@ -47,3 +18,7 @@
</button> </button>
</v-dialog-actions> </v-dialog-actions>
</v-dialog> </v-dialog>
<ng-template #cashTemplate let-cashControl="control">
<cc-cash-field [formControl]="cashControl"></cc-cash-field>
</ng-template>

View File

@ -1,12 +1,25 @@
import { ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
Component,
DestroyRef,
viewChild,
TemplateRef,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Validators, NonNullableFormBuilder } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { DepositParams } from '@vality/fistful-proto/deposit';
import { Revert } from '@vality/fistful-proto/internal/deposit_revert'; import { Revert } from '@vality/fistful-proto/internal/deposit_revert';
import { DialogSuperclass, NotifyLogService, toMinor, clean } from '@vality/ng-core'; import { DialogSuperclass, NotifyLogService, clean } from '@vality/ng-core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject, of } from 'rxjs';
import { Overwrite } from 'utility-types';
import { DepositManagementService } from '@cc/app/api/deposit'; import { DepositManagementService } from '@cc/app/api/deposit';
import { Cash } from '../../../../../components/cash-field';
import {
MetadataFormExtension,
isTypeWithAliases,
} from '../../../../shared/components/metadata-form';
import { UserInfoBasedIdGeneratorService } from '../../../../shared/services'; import { UserInfoBasedIdGeneratorService } from '../../../../shared/services';
import { CreateRevertDialogConfig } from './types/create-revert-dialog-config'; import { CreateRevertDialogConfig } from './types/create-revert-dialog-config';
@ -21,16 +34,24 @@ export class CreateRevertDialogComponent extends DialogSuperclass<
CreateRevertDialogConfig, CreateRevertDialogConfig,
Revert Revert
> { > {
form = this.fb.group({ control = new FormControl({
amount: [undefined as number, [Validators.pattern(/^\d+([,.]\d{1,2})?$/)]], id: this.idGenerator.getUsernameBasedId(),
currency: this.dialogData.currency, body: { currencyCode: this.dialogData.currency },
reason: undefined as string, } as Overwrite<DepositParams, { body: Cash }>);
externalID: undefined as string,
});
progress$ = new BehaviorSubject(0); progress$ = new BehaviorSubject(0);
cashTemplate = viewChild<TemplateRef<unknown>>('cashTemplate');
extensions: MetadataFormExtension[] = [
{
determinant: (data) => of(isTypeWithAliases(data, 'RevertID', 'deposit_revert')),
extension: () => of({ hidden: true }),
},
{
determinant: (data) => of(isTypeWithAliases(data, 'Cash', 'base')),
extension: () => of({ template: this.cashTemplate() }),
},
];
constructor( constructor(
private fb: NonNullableFormBuilder,
private depositManagementService: DepositManagementService, private depositManagementService: DepositManagementService,
private idGenerator: UserInfoBasedIdGeneratorService, private idGenerator: UserInfoBasedIdGeneratorService,
private log: NotifyLogService, private log: NotifyLogService,
@ -40,21 +61,17 @@ export class CreateRevertDialogComponent extends DialogSuperclass<
} }
createRevert() { createRevert() {
const { reason, amount, currency, externalID } = this.form.value; const { body, ...value } = this.control.value;
this.depositManagementService this.depositManagementService
.CreateRevert( .CreateRevert(
this.dialogData.depositID, this.dialogData.depositID,
clean( clean(
{ {
id: this.idGenerator.getUsernameBasedId(), ...value,
body: { body: {
amount: toMinor(amount, currency), currency: { symbolic_code: body.currencyCode },
currency: { amount: body.amount,
symbolic_code: currency,
},
}, },
reason,
external_id: externalID,
}, },
false, false,
true, true,

View File

@ -10,6 +10,9 @@ import { DialogModule } from '@vality/ng-core';
import { UserInfoBasedIdGeneratorModule } from '@cc/app/shared/services/user-info-based-id-generator/user-info-based-id-generator.module'; import { UserInfoBasedIdGeneratorModule } from '@cc/app/shared/services/user-info-based-id-generator/user-info-based-id-generator.module';
import { CashFieldComponent } from '../../../../../components/cash-field';
import { FistfulThriftFormComponent } from '../../../../shared/components/fistful-thrift-form';
import { CreateRevertDialogComponent } from './create-revert-dialog.component'; import { CreateRevertDialogComponent } from './create-revert-dialog.component';
@NgModule({ @NgModule({
@ -23,6 +26,8 @@ import { CreateRevertDialogComponent } from './create-revert-dialog.component';
MatInputModule, MatInputModule,
UserInfoBasedIdGeneratorModule, UserInfoBasedIdGeneratorModule,
DialogModule, DialogModule,
FistfulThriftFormComponent,
CashFieldComponent,
], ],
declarations: [CreateRevertDialogComponent], declarations: [CreateRevertDialogComponent],
}) })

View File

@ -4,7 +4,7 @@ import { DialogService, Column } from '@vality/ng-core';
import startCase from 'lodash-es/startCase'; import startCase from 'lodash-es/startCase';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { getDepositStatus, createCurrencyColumn } from '@cc/app/shared/utils'; import { createCurrencyColumn } from '@cc/app/shared/utils';
import { getUnionKey } from '../../../../utils'; import { getUnionKey } from '../../../../utils';
@ -70,7 +70,7 @@ export class RevertsComponent implements OnInit {
} }
isCreateRevertAvailable(status: DepositStatus): boolean { isCreateRevertAvailable(status: DepositStatus): boolean {
return getDepositStatus(status) !== 'succeeded'; return getUnionKey(status) !== 'succeeded';
} }
update() { update() {

View File

@ -1,12 +1,11 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { NotifyLogService } from '@vality/ng-core';
import { merge, ReplaySubject, Subject, EMPTY } from 'rxjs'; import { merge, ReplaySubject, Subject, EMPTY } from 'rxjs';
import { catchError, switchMap, shareReplay, map } from 'rxjs/operators'; import { catchError, switchMap, shareReplay, map } from 'rxjs/operators';
import { FistfulStatisticsService, createDsl } from '@cc/app/api/fistful-stat'; import { FistfulStatisticsService, createDsl } from '@cc/app/api/fistful-stat';
import { progress } from '@cc/app/shared/custom-operators'; import { progress } from '@cc/app/shared/custom-operators';
import { NotificationErrorService } from '../../../../shared/services/notification-error';
@Injectable() @Injectable()
export class ReceiveDepositService { export class ReceiveDepositService {
private receiveDeposit$ = new ReplaySubject<string>(); private receiveDeposit$ = new ReplaySubject<string>();
@ -20,7 +19,7 @@ export class ReceiveDepositService {
.pipe( .pipe(
catchError((err) => { catchError((err) => {
this.error$.next(true); this.error$.next(true);
this.notificationErrorService.error(err); this.log.error(err);
return EMPTY; return EMPTY;
}), }),
), ),
@ -34,7 +33,7 @@ export class ReceiveDepositService {
constructor( constructor(
private fistfulStatisticsService: FistfulStatisticsService, private fistfulStatisticsService: FistfulStatisticsService,
private notificationErrorService: NotificationErrorService, private log: NotifyLogService,
) {} ) {}
receiveDeposit(id: string) { receiveDeposit(id: string) {

View File

@ -19,6 +19,6 @@
</v-dialog-actions> </v-dialog-actions>
</v-dialog> </v-dialog>
<ng-template #sourceCashTemplate let-control="control"> <ng-template #sourceCashTemplate let-cashControl="control">
<cc-source-cash-field [formControl]="control"></cc-source-cash-field> <cc-source-cash-field [formControl]="cashControl"></cc-source-cash-field>
</ng-template> </ng-template>

View File

@ -58,7 +58,10 @@ export class DepositsComponent implements OnInit {
columns: Column<StatDeposit>[] = [ columns: Column<StatDeposit>[] = [
{ {
field: 'id', field: 'id',
formatter: (d) => d.description || `#${d.id}`,
link: (d) => `/deposits/${d.id}`, link: (d) => `/deposits/${d.id}`,
description: 'id',
maxWidth: 'max(300px, 30vw)',
}, },
{ {
field: 'status', field: 'status',

View File

@ -1,35 +1,34 @@
<ng-container *ngIf="view?.items$ | async as items"> <ng-container *ngIf="!extension?.hidden">
<div <div
*ngIf="!(view.isValue$ | async); else onlyValue" *ngIf="!(view.isValue$ | async); else onlyValue"
style="display: grid; grid-template-columns: 1fr; gap: 16px" style="display: grid; grid-template-columns: 1fr; gap: 16px"
> >
<div <div *ngIf="(view.leaves$ | async)?.length as count" class="grid-container">
*ngIf="(view.leaves$ | async)?.length as count" <div [ngClass]="['grid', 'grid-columns-' + count]">
[ngStyle]="{ <ng-container *ngFor="let item of view.leaves$ | async; let i = index">
display: 'grid', <div
gap: '16px', *ngIf="!((item.current$ | async)?.extension$ | async)?.hidden"
'grid-template-columns': style="display: grid; grid-template-rows: auto 1fr; gap: 8px"
count === 1 ? '1fr' : count === 2 ? '1fr 1fr' : '1fr 1fr 1fr' >
}" <cc-key
> [keys]="item.path$ | async"
<div class="mat-caption mat-secondary-text"
*ngFor="let item of view.leaves$ | async; let i = index" ></cc-key>
style="display: grid; grid-template-rows: auto 1fr; gap: 8px"
>
<cc-key [keys]="item.path$ | async" class="mat-caption mat-secondary-text"></cc-key>
<div *ngIf="item.current$ | async as current" class="mat-body-1"> <div *ngIf="item.current$ | async as current" class="mat-body-1">
<cc-json-viewer <cc-json-viewer
*ngIf="!(current.isEmpty$ | async); else empty" *ngIf="!(current.isEmpty$ | async); else empty"
[data]="current.data$ | async" [data]="current.data$ | async"
[extension]="current.extension$ | async" [extension]="current.extension$ | async"
[extensions]="extensions" [extensions]="extensions"
[value]="current.value$ | async" [value]="current.value$ | async"
></cc-json-viewer> ></cc-json-viewer>
<ng-template #empty> <ng-template #empty>
<mat-icon class="mat-secondary-text" inline>hide_source</mat-icon> <mat-icon class="mat-secondary-text" inline>hide_source</mat-icon>
</ng-template> </ng-template>
</div> </div>
</div>
</ng-container>
</div> </div>
</div> </div>
@ -68,25 +67,36 @@
</div> </div>
</ng-container> </ng-container>
</div> </div>
<ng-template #onlyValue>
<span
[ngClass]="{
link: !!extension?.click || !!extension?.link?.[0],
'tooltip-link': !!extension?.tooltip,
'mat-body-1': true
}"
[queryParams]="extensionQueryParams$ | async"
[routerLink]="extension?.link?.[0]"
(click)="extension?.click?.()"
>
<span
*ngIf="extension?.tooltip; else simpleValue"
[matTooltip]="getTooltip(extension.tooltip)"
matTooltipClass="tooltip"
>{{ view.renderValue$ | async }}</span
>
<ng-template #simpleValue>{{ view.renderValue$ | async }}</ng-template>
</span>
</ng-template>
</ng-container> </ng-container>
<ng-template #onlyValue>
<span
[queryParams]="extensionQueryParams$ | async"
[routerLink]="extension?.link?.[0]"
(click)="extension?.click?.()"
>
<span
*ngIf="extension?.tooltip; else tagValue"
[matTooltip]="getTooltip(extension.tooltip)"
matTooltipClass="tooltip"
>
<ng-container *ngTemplateOutlet="tagValue"></ng-container>
</span>
<ng-template #tagValue>
<v-tag *ngIf="extension?.tag; else simpleValue" [color]="extension?.color">
<ng-container *ngTemplateOutlet="simpleValue"></ng-container>
</v-tag>
</ng-template>
<ng-template #simpleValue>
<span
[ngClass]="{
link: !!extension?.click || !!extension?.link?.[0],
'tooltip-link': !!extension?.tooltip,
'mat-body-1': !extension?.tag
}"
>
{{ view.renderValue$ | async }}
</span>
</ng-template>
</span>
</ng-template>

View File

@ -7,6 +7,7 @@ import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { TagModule } from '@vality/ng-core';
import { ThriftPipesModule } from '@cc/app/shared'; import { ThriftPipesModule } from '@cc/app/shared';
import { DetailsItemModule } from '@cc/components/details-item'; import { DetailsItemModule } from '@cc/components/details-item';
@ -28,6 +29,7 @@ import { JsonViewerComponent } from './json-viewer.component';
MatTooltipModule, MatTooltipModule,
MatBadgeModule, MatBadgeModule,
RouterModule, RouterModule,
TagModule,
], ],
}) })
export class JsonViewerModule {} export class JsonViewerModule {}

View File

@ -11,11 +11,61 @@
.link, .link,
.tooltip-link { .tooltip-link {
text-decoration: underline; text-decoration: underline;
}
.tooltip-link {
text-decoration-style: dotted; text-decoration-style: dotted;
cursor: default; cursor: default;
} }
.link { .link {
text-decoration-style: solid !important; text-decoration-style: solid;
cursor: pointer !important; cursor: pointer;
}
.grid-container {
container-type: inline-size;
.grid {
display: grid;
gap: 16px;
grid-template-columns: repeat(4, 1fr);
&.grid-columns-1 {
grid-template-columns: 1fr;
}
&.grid-columns-2 {
grid-template-columns: repeat(2, 1fr);
}
&.grid-columns-3 {
grid-template-columns: repeat(3, 1fr);
}
}
}
@container (width < 1200px) {
.grid-container > .grid {
grid-template-columns: repeat(3, 1fr);
}
}
@container (width < 900px) {
.grid-container > .grid {
&,
&.grid-columns-3 {
grid-template-columns: repeat(2, 1fr);
}
}
}
@container (width < 600px) {
.grid-container > .grid {
&,
&.grid-columns-3,
&.grid-columns-2 {
grid-template-columns: 1fr;
}
}
} }

View File

@ -1,4 +1,5 @@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Color } from '@vality/ng-core';
import { Observable, combineLatest, switchMap, of } from 'rxjs'; import { Observable, combineLatest, switchMap, of } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -6,26 +7,34 @@ import { MetadataFormData } from '../../metadata-form';
export interface MetadataViewExtensionResult { export interface MetadataViewExtensionResult {
key?: string; key?: string;
value: string; value?: string;
hidden?: boolean;
tooltip?: unknown; tooltip?: unknown;
link?: Parameters<Router['navigate']>; link?: Parameters<Router['navigate']>;
click?: () => void; click?: () => void;
color?: Color;
tag?: boolean;
} }
export type MetadataViewExtension = { export type MetadataViewExtension = {
determinant: (data: MetadataFormData, value: unknown) => Observable<boolean>; determinant: (data: MetadataFormData, value: unknown) => Observable<boolean>;
extension: (data: MetadataFormData, value: unknown) => Observable<MetadataViewExtensionResult>; extension: (
data: MetadataFormData,
value: unknown,
viewValue: unknown,
) => Observable<MetadataViewExtensionResult>;
}; };
export function getFirstDeterminedExtensionsResult( export function getFirstDeterminedExtensionsResult(
sourceExtensions: MetadataViewExtension[], sourceExtensions: MetadataViewExtension[],
data: MetadataFormData, data: MetadataFormData,
value: unknown, value: unknown,
viewValue: unknown,
): Observable<MetadataViewExtensionResult> { ): Observable<MetadataViewExtensionResult> {
return sourceExtensions?.length return sourceExtensions?.length
? combineLatest(sourceExtensions.map(({ determinant }) => determinant(data, value))).pipe( ? combineLatest(sourceExtensions.map(({ determinant }) => determinant(data, value))).pipe(
map((determined) => sourceExtensions.find((_, idx) => determined[idx])), map((determined) => sourceExtensions.find((_, idx) => determined[idx])),
switchMap((extension) => extension?.extension(data, value) ?? of(null)), switchMap((extension) => extension?.extension(data, value, viewValue) ?? of(null)),
) )
: of(null); : of(null);
} }

View File

@ -2,8 +2,8 @@ import { isEmpty } from '@vality/ng-core';
import { SetType, ListType, MapType, ValueType } from '@vality/thrift-ts'; import { SetType, ListType, MapType, ValueType } from '@vality/thrift-ts';
import isNil from 'lodash-es/isNil'; import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject'; import isObject from 'lodash-es/isObject';
import { Observable, of, switchMap, combineLatest } from 'rxjs'; import { Observable, of, switchMap, combineLatest, defer } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators'; import { map, shareReplay, distinctUntilChanged, startWith } from 'rxjs/operators';
import { MetadataFormData } from '../../metadata-form'; import { MetadataFormData } from '../../metadata-form';
@ -12,42 +12,34 @@ import { getEntries } from './get-entries';
import { import {
MetadataViewExtension, MetadataViewExtension,
getFirstDeterminedExtensionsResult, getFirstDeterminedExtensionsResult,
MetadataViewExtensionResult,
} from './metadata-view-extension'; } from './metadata-view-extension';
export class MetadataViewItem { export class MetadataViewItem {
extension$ = getFirstDeterminedExtensionsResult(this.extensions, this.data, this.value).pipe( extension$: Observable<MetadataViewExtensionResult> = defer(() => this.renderValue$).pipe(
switchMap((viewValue) =>
getFirstDeterminedExtensionsResult(this.extensions, this.data, this.value, viewValue),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
data$ = this.extension$.pipe(
startWith(null),
map((ext) => (ext ? null : this.data)),
shareReplay({ refCount: true, bufferSize: 1 }), shareReplay({ refCount: true, bufferSize: 1 }),
); );
data$ = this.extension$.pipe(map((ext) => (ext ? null : this.data)));
key$ = this.extension$.pipe( key$ = this.extension$.pipe(
map((ext) => (isNil(ext?.key) ? this.key : new MetadataViewItem(ext.key))), map((ext) => (isNil(ext?.key) ? this.key : new MetadataViewItem(ext.key))),
shareReplay({ refCount: true, bufferSize: 1 }), shareReplay({ refCount: true, bufferSize: 1 }),
); );
value$ = this.extension$.pipe( value$ = this.extension$.pipe(
map((ext) => { startWith(null),
const value = ext?.value ?? this.value; map((ext) => this.getValue(ext)),
return isEmpty(value) ? null : value; shareReplay({ refCount: true, bufferSize: 1 }),
}),
); );
renderValue$ = combineLatest([this.value$, this.data$]).pipe( renderValue$ = combineLatest([this.value$, this.data$]).pipe(
map(([value, data]) => { map(([value, data]) => this.getRenderValue(value, data)),
if (data?.trueTypeNode?.data?.objectType === 'enum') { distinctUntilChanged(),
return ( shareReplay({ refCount: true, bufferSize: 1 }),
(data.trueTypeNode.data as MetadataFormData<ValueType, 'enum'>).ast.items.find(
(i, idx) => {
if ('value' in i) {
return i.value === value;
}
return idx === value;
},
).name ?? value
);
}
if (data?.objectType === 'union' && isEmpty(getEntries(value)?.[0]?.[1])) {
return getEntries(value)?.[0]?.[0];
}
return value;
}),
); );
isEmpty$ = this.renderValue$.pipe(map((value) => isEmpty(value))); isEmpty$ = this.renderValue$.pipe(map((value) => isEmpty(value)));
@ -199,4 +191,28 @@ export class MetadataViewItem {
}), }),
); );
} }
private getValue(ext: MetadataViewExtensionResult) {
const value = ext?.value ?? this.value;
return isEmpty(value) || ext?.hidden ? null : value;
}
private getRenderValue(value: unknown, data: MetadataFormData) {
if (data?.trueTypeNode?.data?.objectType === 'enum') {
return (
(data.trueTypeNode.data as MetadataFormData<ValueType, 'enum'>).ast.items.find(
(i, idx) => {
if ('value' in i) {
return i.value === value;
}
return idx === value;
},
).name ?? value
);
}
if (data?.objectType === 'union' && isEmpty(getEntries(value)?.[0]?.[1])) {
return getEntries(value)?.[0]?.[0];
}
return value;
}
} }

View File

@ -24,7 +24,7 @@ import {
import { JsonViewerModule } from '@cc/app/shared/components/json-viewer'; import { JsonViewerModule } from '@cc/app/shared/components/json-viewer';
import { ThriftPipesModule } from '@cc/app/shared/pipes/thrift'; import { ThriftPipesModule } from '@cc/app/shared/pipes/thrift';
import { ValueTypeTitleModule } from '@cc/app/shared/pipes/value-type-title'; import { ValueTypeTitleModule } from '@cc/app/shared/pipes/value-type-title';
import { CashModule } from '@cc/components/cash-field'; import { CashFieldComponent } from '@cc/components/cash-field';
import { ComplexFormComponent } from './components/complex-form/complex-form.component'; import { ComplexFormComponent } from './components/complex-form/complex-form.component';
import { EnumFieldComponent } from './components/enum-field/enum-field.component'; import { EnumFieldComponent } from './components/enum-field/enum-field.component';
@ -58,7 +58,7 @@ import { FieldLabelPipe } from './pipes/field-label.pipe';
MatDatepickerModule, MatDatepickerModule,
DatetimeFieldModule, DatetimeFieldModule,
PipesModule, PipesModule,
CashModule, CashFieldComponent,
AutocompleteFieldModule, AutocompleteFieldModule,
TagModule, TagModule,
], ],

View File

@ -1,9 +0,0 @@
import { DepositStatus } from '@vality/fistful-proto/fistful_stat';
import { clearNullFields } from '@cc/utils/thrift-utils';
/**
* @deprecated use get union key
*/
export const getDepositStatus = (status: DepositStatus): string =>
Object.keys(clearNullFields(status))[0];

View File

@ -1,4 +1,2 @@
export * from './polling-conditions';
export * from './deposit-status';
export * from './clean-thrift'; export * from './clean-thrift';
export * from './table'; export * from './table';

View File

@ -1,6 +0,0 @@
import { StatDeposit } from '@vality/fistful-proto/fistful_stat';
import { getDepositStatus } from './deposit-status';
export const createDepositStopPollingCondition = (deposit: StatDeposit): boolean =>
!!deposit && getDepositStatus(deposit.status) !== 'pending';

View File

@ -9,25 +9,10 @@
matInput matInput
/> />
</mat-form-field> </mat-form-field>
<mat-form-field style="min-width: 28px; width: 50%"> <v-select-field
<mat-label>Currency</mat-label> [formControl]="currencyControl"
<input [options]="options$ | async"
[formControl]="currencyCodeControl" [style.width.px]="200"
[inputMask]="currencyMask" label="Currency"
[matAutocomplete]="auto" ></v-select-field>
[required]="required"
matInput
/>
<mat-autocomplete
#auto="matAutocomplete"
(optionSelected)="this.currencyCodeControl.setValue($event.option.value)"
>
<mat-option
*ngFor="let currency of currencies$ | async"
[value]="currency.data.symbolic_code"
>
{{ currency.data.symbolic_code }} ({{ currency.data.name }})
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div> </div>

View File

@ -1,8 +1,7 @@
import { getCurrencySymbol } from '@angular/common'; import { getCurrencySymbol, CommonModule } from '@angular/common';
import { import {
Component, Component,
Input, Input,
Injector,
Inject, Inject,
LOCALE_ID, LOCALE_ID,
OnInit, OnInit,
@ -10,14 +9,26 @@ import {
DestroyRef, DestroyRef,
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Validator, ValidationErrors, FormControl } from '@angular/forms'; import { Validator, ValidationErrors, FormControl, ReactiveFormsModule } from '@angular/forms';
import { createMask } from '@ngneat/input-mask'; import { MatFormField } from '@angular/material/form-field';
import { FormComponentSuperclass, createControlProviders, getValueChanges } from '@vality/ng-core'; import { MatInputModule } from '@angular/material/input';
import sortBy from 'lodash-es/sortBy'; import { createMask, InputMaskModule } from '@ngneat/input-mask';
import { CurrencyObject } from '@vality/domain-proto/domain';
import {
FormComponentSuperclass,
createControlProviders,
getValueChanges,
Option,
SelectFieldModule,
toMinorByExponent,
toMajorByExponent,
compareDifferentTypes,
} from '@vality/ng-core';
import isNil from 'lodash-es/isNil';
import { combineLatest } from 'rxjs'; import { combineLatest } from 'rxjs';
import { map, switchMap, first, distinctUntilChanged } from 'rxjs/operators'; import { map, distinctUntilChanged, shareReplay, startWith, take } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config'; import { DomainStoreService } from '../../app/api/domain-config';
export interface Cash { export interface Cash {
amount: number; amount: number;
@ -25,104 +36,147 @@ export interface Cash {
} }
const GROUP_SEPARATOR = ' '; const GROUP_SEPARATOR = ' ';
const DEFAULT_EXPONENT = 2;
const RADIX_POINT = '.';
@Component({ @Component({
standalone: true,
selector: 'cc-cash-field', selector: 'cc-cash-field',
templateUrl: './cash-field.component.html', templateUrl: './cash-field.component.html',
providers: createControlProviders(() => CashFieldComponent), providers: createControlProviders(() => CashFieldComponent),
imports: [
MatFormField,
ReactiveFormsModule,
InputMaskModule,
SelectFieldModule,
CommonModule,
MatInputModule,
],
}) })
export class CashFieldComponent extends FormComponentSuperclass<Cash> implements Validator, OnInit { export class CashFieldComponent extends FormComponentSuperclass<Cash> implements Validator, OnInit {
@Input() label?: string; @Input() label?: string;
@Input({ transform: booleanAttribute }) required: boolean = false; @Input({ transform: booleanAttribute }) required: boolean = false;
@Input({ transform: booleanAttribute }) minor: boolean = false;
amountControl = new FormControl<string>(null); amountControl = new FormControl<string>(null);
currencyCodeControl = new FormControl<string>(null); currencyControl = new FormControl<CurrencyObject>(null);
currencies$ = combineLatest([ options$ = this.domainStoreService.getObjects('currency').pipe(
getValueChanges(this.currencyCodeControl), startWith([] as CurrencyObject[]),
this.domainStoreService.getObjects('currency'), map((objs): Option<CurrencyObject>[] =>
]).pipe( objs
map(([code, currencies]) => .sort((a, b) => compareDifferentTypes(a.data.symbolic_code, b.data.symbolic_code))
sortBy(currencies, 'data', 'symbolic_code').filter( .map((s) => ({
(c) => label: s.data.symbolic_code,
c.data.symbolic_code.toUpperCase().includes(code) || c.data.name.includes(code), description: s.data.name,
), value: s,
})),
), ),
shareReplay({ refCount: true, bufferSize: 1 }),
); );
currencyExponent$ = getValueChanges(this.currencyControl).pipe(
amountMask$ = getValueChanges(this.currencyCodeControl).pipe( map((obj) => obj?.data?.exponent ?? DEFAULT_EXPONENT),
switchMap((code) => this.getCurrencyByCode(code)),
map((c) => (this.minor ? 0 : c?.data?.exponent || 2)),
distinctUntilChanged(), distinctUntilChanged(),
map((digits) => shareReplay({ refCount: true, bufferSize: 1 }),
);
amountMask$ = this.currencyExponent$.pipe(
distinctUntilChanged(),
map((exponent) =>
createMask({ createMask({
alias: 'numeric', alias: 'numeric',
groupSeparator: GROUP_SEPARATOR, groupSeparator: GROUP_SEPARATOR,
digits, digits: exponent,
digitsOptional: true, digitsOptional: true,
placeholder: '', placeholder: '',
onBeforePaste: (pastedValue: string) =>
this.convertPastedToStringNumber(pastedValue),
}), }),
), ),
shareReplay({ refCount: true, bufferSize: 1 }),
); );
currencyMask = createMask({ mask: 'AAA', placeholder: '' });
get currencyCode() {
return this.currencyControl.value?.data?.symbolic_code;
}
get prefix() { get prefix() {
return getCurrencySymbol(this.currencyCodeControl.value, 'narrow', this._locale); return getCurrencySymbol(this.currencyCode, 'narrow', this._locale);
} }
constructor( constructor(
injector: Injector,
@Inject(LOCALE_ID) private _locale: string, @Inject(LOCALE_ID) private _locale: string,
private domainStoreService: DomainStoreService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private domainStoreService: DomainStoreService,
) { ) {
super(injector); super();
} }
ngOnInit() { ngOnInit() {
super.ngOnInit();
combineLatest([ combineLatest([
getValueChanges(this.currencyCodeControl), combineLatest([getValueChanges(this.amountControl), this.currencyExponent$]).pipe(
getValueChanges(this.amountControl), map(([amountStr, exponent]) => {
const amount = amountStr
? Number(amountStr.replaceAll(GROUP_SEPARATOR, ''))
: null;
return isNil(amount) ? null : toMinorByExponent(amount, exponent);
}),
distinctUntilChanged(),
),
getValueChanges(this.currencyControl).pipe(
map((obj) => obj?.data?.symbolic_code),
distinctUntilChanged(),
),
]) ])
.pipe( .pipe(
switchMap(([currencyCode]) => this.getCurrencyByCode(currencyCode)), map(([amount, currencyCode]) =>
!isNil(amount) && currencyCode ? { amount, currencyCode } : null,
),
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef), takeUntilDestroyed(this.destroyRef),
) )
.subscribe((currency) => { .subscribe((value) => {
const amountStr = this.amountControl.value; this.emitOutgoingValue(value);
if (amountStr && currency && !this.validate()) {
const [whole, fractional] = amountStr.split('.');
if (fractional?.length > currency.data.exponent) {
this.amountControl.setValue(
`${whole}.${fractional.slice(0, currency.data.exponent)}`,
);
}
const amount = Number(this.amountControl.value.replaceAll(GROUP_SEPARATOR, ''));
this.emitOutgoingValue({ amount, currencyCode: currency.data.symbolic_code });
} else {
this.emitOutgoingValue(null);
}
}); });
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return !this.amountControl.value || this.currencyCodeControl.value?.length !== 3 return !this.amountControl.value || !this.currencyControl.value
? { invalidCash: true } ? { invalidCash: true }
: null; : null;
} }
handleIncomingValue(value: Cash) { handleIncomingValue(value: Cash) {
this.amountControl.setValue( const { currencyCode, amount } = value || {};
typeof value?.amount === 'number' ? String(value.amount) : null, if (!currencyCode) {
); this.setValues(amount, null);
this.currencyCodeControl.setValue(value?.currencyCode); }
this.options$
.pipe(
map(
(options) =>
options.find((o) => o.value?.data?.symbolic_code === value.currencyCode)
?.value ?? null,
),
take(1),
takeUntilDestroyed(this.destroyRef),
)
.subscribe((obj) => {
this.setValues(amount, obj);
});
} }
private getCurrencyByCode(currencyCode: string) { private setValues(amount: number, currencyObject: CurrencyObject) {
return this.domainStoreService.getObjects('currency').pipe( this.currencyControl.setValue(currencyObject);
map((c) => c.find((v) => v.data.symbolic_code === currencyCode)), this.amountControl.setValue(
first(), typeof amount === 'number'
? String(
toMajorByExponent(amount, currencyObject?.data?.exponent ?? DEFAULT_EXPONENT),
)
: null,
); );
} }
private convertPastedToStringNumber(pastedValue: string) {
return pastedValue.replaceAll(',', RADIX_POINT);
}
} }

View File

@ -1,22 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input';
import { InputMaskModule } from '@ngneat/input-mask';
import { CashFieldComponent } from './cash-field.component';
@NgModule({
declarations: [CashFieldComponent],
imports: [
CommonModule,
FormsModule,
MatInputModule,
MatAutocompleteModule,
InputMaskModule,
ReactiveFormsModule,
],
exports: [CashFieldComponent],
})
export class CashModule {}

View File

@ -1,2 +1 @@
export * from './cash-field.module';
export * from './cash-field.component'; export * from './cash-field.component';

View File

@ -25,7 +25,7 @@ import {
} from '@vality/ng-core'; } from '@vality/ng-core';
import isNil from 'lodash-es/isNil'; import isNil from 'lodash-es/isNil';
import { combineLatest, switchMap, of } from 'rxjs'; import { combineLatest, switchMap, of } from 'rxjs';
import { map, first, distinctUntilChanged, shareReplay, startWith } from 'rxjs/operators'; import { map, distinctUntilChanged, shareReplay, startWith, take } from 'rxjs/operators';
import { DomainStoreService } from '../../app/api/domain-config'; import { DomainStoreService } from '../../app/api/domain-config';
import { FetchSourcesService } from '../../app/sections/sources'; import { FetchSourcesService } from '../../app/sections/sources';
@ -36,6 +36,7 @@ export interface SourceCash {
} }
const GROUP_SEPARATOR = ' '; const GROUP_SEPARATOR = ' ';
const DEFAULT_EXPONENT = 2;
const RADIX_POINT = '.'; const RADIX_POINT = '.';
@Component({ @Component({
@ -159,7 +160,7 @@ export class SourceCashFieldComponent
switchMap((s) => switchMap((s) =>
combineLatest([of(s), this.getCurrencyExponent(s?.currency_symbolic_code)]), combineLatest([of(s), this.getCurrencyExponent(s?.currency_symbolic_code)]),
), ),
first(), take(1),
takeUntilDestroyed(this.destroyRef), takeUntilDestroyed(this.destroyRef),
) )
.subscribe(([source, exponent]) => { .subscribe(([source, exponent]) => {
@ -174,12 +175,12 @@ export class SourceCashFieldComponent
map( map(
(currencies) => (currencies) =>
currencies.find((c) => c.data.symbolic_code === symbolicCode)?.data currencies.find((c) => c.data.symbolic_code === symbolicCode)?.data
?.exponent ?? 2, ?.exponent ?? DEFAULT_EXPONENT,
), ),
); );
} }
private setValues(amount: number, source: StatSource, exponent: number = 2) { private setValues(amount: number, source: StatSource, exponent: number = DEFAULT_EXPONENT) {
this.sourceControl.setValue(source); this.sourceControl.setValue(source);
this.amountControl.setValue( this.amountControl.setValue(
typeof amount === 'number' ? String(toMajorByExponent(amount, exponent)) : null, typeof amount === 'number' ? String(toMajorByExponent(amount, exponent)) : null,