mirror of
https://github.com/valitydev/control-center.git
synced 2024-11-06 02:25:17 +00:00
TD-844: New create deposit (#324)
This commit is contained in:
parent
78204f8696
commit
52e8d642f6
8
package-lock.json
generated
8
package-lock.json
generated
@ -22,7 +22,7 @@
|
||||
"@ngneat/input-mask": "6.0.0",
|
||||
"@vality/deanonimus-proto": "2.0.1-2a02d87.0",
|
||||
"@vality/domain-proto": "2.0.1-decfa45.0",
|
||||
"@vality/fistful-proto": "2.0.1-23e9ba3.0",
|
||||
"@vality/fistful-proto": "2.0.1-8ecf2b7.0",
|
||||
"@vality/machinegun-proto": "1.0.0",
|
||||
"@vality/magista-proto": "2.0.2-28d11b9.0",
|
||||
"@vality/ng-core": "^17.1.1-pr-57-4da820a.0",
|
||||
@ -6430,9 +6430,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vality/fistful-proto": {
|
||||
"version": "2.0.1-23e9ba3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/fistful-proto/-/fistful-proto-2.0.1-23e9ba3.0.tgz",
|
||||
"integrity": "sha512-yOEV1tR7977GWLVUKThSYmC7KnwaswIqQpIacW5ffUdKMCvp+uv9LZZHpTd812k6WZXcj/NbGMIdEteQHqCLXA=="
|
||||
"version": "2.0.1-8ecf2b7.0",
|
||||
"resolved": "https://registry.npmjs.org/@vality/fistful-proto/-/fistful-proto-2.0.1-8ecf2b7.0.tgz",
|
||||
"integrity": "sha512-F2nR/OIq3dFhVW+Y1hQROMtma5Pyj50grS2GNs8gvNbKzlEIMN8Osvdr6Z5khFbuctfB8GetuWldg/fGm3k8KQ=="
|
||||
},
|
||||
"node_modules/@vality/machinegun-proto": {
|
||||
"version": "1.0.0",
|
||||
|
@ -30,7 +30,7 @@
|
||||
"@ngneat/input-mask": "6.0.0",
|
||||
"@vality/deanonimus-proto": "2.0.1-2a02d87.0",
|
||||
"@vality/domain-proto": "2.0.1-decfa45.0",
|
||||
"@vality/fistful-proto": "2.0.1-23e9ba3.0",
|
||||
"@vality/fistful-proto": "2.0.1-8ecf2b7.0",
|
||||
"@vality/machinegun-proto": "1.0.0",
|
||||
"@vality/magista-proto": "2.0.2-28d11b9.0",
|
||||
"@vality/ng-core": "^17.1.1-pr-57-4da820a.0",
|
||||
|
@ -4,9 +4,10 @@ import {
|
||||
ThriftAstMetadata,
|
||||
deposit_Management,
|
||||
} from '@vality/fistful-proto';
|
||||
import { DepositID } from '@vality/fistful-proto/deposit';
|
||||
import { DepositID, DepositParams } from '@vality/fistful-proto/deposit';
|
||||
import { AdjustmentParams, AdjustmentState } from '@vality/fistful-proto/deposit_adjustment';
|
||||
import { RevertParams, RevertState } from '@vality/fistful-proto/deposit_revert';
|
||||
import { ContextSet } from '@vality/fistful-proto/internal/context';
|
||||
import { combineLatest, from, map, Observable, switchMap } from 'rxjs';
|
||||
|
||||
import { KeycloakTokenInfoService, toWachterHeaders } from '@cc/app/shared/services';
|
||||
@ -15,7 +16,7 @@ import { environment } from '@cc/environments/environment';
|
||||
import { ConfigService } from '../../core/config.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ManagementService {
|
||||
export class DepositManagementService {
|
||||
private client$: Observable<deposit_ManagementCodegenClient>;
|
||||
|
||||
constructor(
|
||||
@ -51,4 +52,9 @@ export class ManagementService {
|
||||
CreateRevert(id: DepositID, params: RevertParams): Observable<RevertState> {
|
||||
return this.client$.pipe(switchMap((c) => c.CreateRevert(id, params)));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
Create(params: DepositParams, context: ContextSet) {
|
||||
return this.client$.pipe(switchMap((c) => c.Create(params, context)));
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
export * from './management.service';
|
||||
export * from './deposit-management.service';
|
||||
|
@ -5,7 +5,7 @@ import { Revert } from '@vality/fistful-proto/internal/deposit_revert';
|
||||
import { DialogSuperclass, NotifyLogService, toMinor, clean } from '@vality/ng-core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { ManagementService } from '@cc/app/api/deposit';
|
||||
import { DepositManagementService } from '@cc/app/api/deposit';
|
||||
|
||||
import { UserInfoBasedIdGeneratorService } from '../../../../shared/services';
|
||||
|
||||
@ -31,7 +31,7 @@ export class CreateRevertDialogComponent extends DialogSuperclass<
|
||||
|
||||
constructor(
|
||||
private fb: NonNullableFormBuilder,
|
||||
private depositManagementService: ManagementService,
|
||||
private depositManagementService: DepositManagementService,
|
||||
private idGenerator: UserInfoBasedIdGeneratorService,
|
||||
private log: NotifyLogService,
|
||||
private destroyRef: DestroyRef,
|
||||
|
@ -1,33 +1,24 @@
|
||||
<v-dialog [progress]="isLoading$ | async" title="Create deposit">
|
||||
<form *ngIf="form" [formGroup]="form" style="display: flex; flex-direction: column; gap: 16px">
|
||||
<mat-form-field>
|
||||
<input formControlName="destination" matInput placeholder="Destination" required />
|
||||
</mat-form-field>
|
||||
<div style="display: flex; gap: 24px">
|
||||
<mat-form-field style="flex: 1">
|
||||
<input
|
||||
formControlName="amount"
|
||||
matInput
|
||||
placeholder="Amount"
|
||||
required
|
||||
type="number"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<cc-currency-source-field
|
||||
formControlName="currency"
|
||||
required
|
||||
style="flex: 1"
|
||||
></cc-currency-source-field>
|
||||
</div>
|
||||
</form>
|
||||
<v-dialog [progress]="progress$ | async" title="Create deposit">
|
||||
<cc-fistful-thrift-form
|
||||
[defaultValue]="getDefaultValue()"
|
||||
[extensions]="extensions"
|
||||
[formControl]="control"
|
||||
namespace="deposit"
|
||||
noChangeKind
|
||||
type="DepositParams"
|
||||
></cc-fistful-thrift-form>
|
||||
<v-dialog-actions>
|
||||
<button
|
||||
[disabled]="(isLoading$ | async) || form.invalid"
|
||||
[disabled]="(progress$ | async) || control.invalid"
|
||||
color="primary"
|
||||
mat-button
|
||||
(click)="createDeposit()"
|
||||
(click)="create()"
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</v-dialog-actions>
|
||||
</v-dialog>
|
||||
|
||||
<ng-template #sourceCashTemplate let-control="control">
|
||||
<cc-source-cash-field [formControl]="control"></cc-source-cash-field>
|
||||
</ng-template>
|
||||
|
@ -1,8 +0,0 @@
|
||||
mat-dialog-actions {
|
||||
margin-bottom: -24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 36px;
|
||||
}
|
@ -1,63 +1,85 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, DestroyRef } from '@angular/core';
|
||||
import { Component, DestroyRef, ViewChild, TemplateRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { StatDeposit } from '@vality/fistful-proto/fistful_stat';
|
||||
import { DialogSuperclass } from '@vality/ng-core';
|
||||
import { FormControl, Validators } from '@angular/forms';
|
||||
import { DepositParams } from '@vality/fistful-proto/deposit';
|
||||
import { DialogSuperclass, NotifyLogService, progressTo } from '@vality/ng-core';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { Overwrite } from 'utility-types';
|
||||
|
||||
import { CreateDepositService } from './services/create-deposit/create-deposit.service';
|
||||
import { SourceCash } from '../../../../components/source-cash-field';
|
||||
import { DepositManagementService } from '../../../api/deposit';
|
||||
import { MetadataFormExtension, isTypeWithAliases } from '../../../shared/components/metadata-form';
|
||||
import { UserInfoBasedIdGeneratorService } from '../../../shared/services';
|
||||
import { FetchSourcesService } from '../../sources';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'create-deposit-dialog.component.html',
|
||||
styleUrls: ['create-deposit-dialog.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [CreateDepositService],
|
||||
})
|
||||
export class CreateDepositDialogComponent
|
||||
extends DialogSuperclass<CreateDepositDialogComponent, void, StatDeposit>
|
||||
implements OnInit
|
||||
{
|
||||
form: UntypedFormGroup;
|
||||
depositCreated$ = this.createDepositService.depositCreated$;
|
||||
isLoading$ = this.createDepositService.isLoading$;
|
||||
error$ = this.createDepositService.error$;
|
||||
pollingError$ = this.createDepositService.pollingError$;
|
||||
pollingTimeout$ = this.createDepositService.pollingTimeout$;
|
||||
export class CreateDepositDialogComponent extends DialogSuperclass<CreateDepositDialogComponent> {
|
||||
@ViewChild('sourceCashTemplate') sourceCashTemplate: TemplateRef<unknown>;
|
||||
|
||||
control = new FormControl(this.getDefaultValue(), [Validators.required]);
|
||||
progress$ = new BehaviorSubject(0);
|
||||
extensions: MetadataFormExtension[] = [
|
||||
{
|
||||
determinant: (data) =>
|
||||
of(
|
||||
isTypeWithAliases(data, 'ContextSet', 'context') ||
|
||||
isTypeWithAliases(data, 'DepositID', 'deposit') ||
|
||||
isTypeWithAliases(data, 'SourceID', 'deposit'),
|
||||
),
|
||||
extension: () => of({ hidden: true }),
|
||||
},
|
||||
{
|
||||
determinant: (data) => of(isTypeWithAliases(data, 'Cash', 'base')),
|
||||
extension: () => of({ template: this.sourceCashTemplate }),
|
||||
},
|
||||
];
|
||||
|
||||
constructor(
|
||||
private createDepositService: CreateDepositService,
|
||||
private snackBar: MatSnackBar,
|
||||
private destroyRef: DestroyRef,
|
||||
private depositManagementService: DepositManagementService,
|
||||
private log: NotifyLogService,
|
||||
private userInfoBasedIdGeneratorService: UserInfoBasedIdGeneratorService,
|
||||
private fetchSourcesService: FetchSourcesService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.createDepositService.form;
|
||||
this.depositCreated$.subscribe((deposit) => {
|
||||
this.snackBar.open(`Deposit status successfully created`, 'OK', { duration: 3000 });
|
||||
this.closeWithSuccess(deposit);
|
||||
this.form.enable();
|
||||
});
|
||||
this.error$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.snackBar.open('An error occurred while deposit create', 'OK');
|
||||
this.closeWithError();
|
||||
this.form.enable();
|
||||
});
|
||||
this.pollingError$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.snackBar.open('An error occurred while deposit polling', 'OK');
|
||||
this.closeWithError();
|
||||
this.form.enable();
|
||||
});
|
||||
this.pollingTimeout$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
|
||||
this.snackBar.open('Polling timeout error', 'OK');
|
||||
this.closeWithError();
|
||||
this.form.enable();
|
||||
});
|
||||
create() {
|
||||
const { body: sourceCash, ...value } = this.control.value;
|
||||
this.fetchSourcesService.sources$
|
||||
.pipe(
|
||||
first(),
|
||||
map(
|
||||
(sources) =>
|
||||
sources.find((s) => s.id === sourceCash.sourceId).currency_symbolic_code,
|
||||
),
|
||||
map(
|
||||
(symbolicCode): DepositParams => ({
|
||||
...value,
|
||||
source_id: sourceCash.sourceId,
|
||||
body: {
|
||||
amount: sourceCash.amount,
|
||||
currency: { symbolic_code: symbolicCode },
|
||||
},
|
||||
}),
|
||||
),
|
||||
switchMap((params) => this.depositManagementService.Create(params, new Map())),
|
||||
progressTo(this.progress$),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe({
|
||||
next: () => this.closeWithSuccess(),
|
||||
error: (err) => this.log.error(err),
|
||||
});
|
||||
}
|
||||
|
||||
createDeposit() {
|
||||
this.form.disable();
|
||||
this.createDepositService.createDeposit();
|
||||
getDefaultValue() {
|
||||
return {
|
||||
id: this.userInfoBasedIdGeneratorService.getUsernameBasedId(),
|
||||
source_id: 'STUB',
|
||||
} as Overwrite<DepositParams, { body: SourceCash }>;
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,10 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
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 { SourceCashFieldComponent } from '../../../../components/source-cash-field';
|
||||
import { CurrencySourceFieldComponent } from '../../../shared/components/currency-source-field';
|
||||
import { FistfulThriftFormComponent } from '../../../shared/components/fistful-thrift-form';
|
||||
import { UserInfoBasedIdGeneratorModule } from '../../../shared/services/user-info-based-id-generator/user-info-based-id-generator.module';
|
||||
|
||||
import { CreateDepositDialogComponent } from './create-deposit-dialog.component';
|
||||
|
||||
@ -28,6 +29,8 @@ import { CreateDepositDialogComponent } from './create-deposit-dialog.component'
|
||||
UserInfoBasedIdGeneratorModule,
|
||||
DialogModule,
|
||||
CurrencySourceFieldComponent,
|
||||
FistfulThriftFormComponent,
|
||||
SourceCashFieldComponent,
|
||||
],
|
||||
declarations: [CreateDepositDialogComponent],
|
||||
})
|
||||
|
@ -1,117 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Validators, FormBuilder } from '@angular/forms';
|
||||
import { DepositParams } from '@vality/fistful-proto/fistful_admin';
|
||||
import { StatDeposit, StatRequest } from '@vality/fistful-proto/fistful_stat';
|
||||
import { StatSource } from '@vality/fistful-proto/internal/fistful_stat';
|
||||
import { getNoTimeZoneIsoString } from '@vality/ng-core';
|
||||
import { endOfDay, startOfDay } from 'date-fns';
|
||||
import { EMPTY, forkJoin, merge, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { FistfulAdminService } from '@cc/app/api/fistful-admin';
|
||||
import { FistfulStatisticsService, createDsl } from '@cc/app/api/fistful-stat';
|
||||
import { progress } from '@cc/app/shared/custom-operators';
|
||||
import { UserInfoBasedIdGeneratorService } from '@cc/app/shared/services';
|
||||
import { createDepositStopPollingCondition } from '@cc/app/shared/utils';
|
||||
import { poll } from '@cc/utils/poll';
|
||||
import { toMinor } from '@cc/utils/to-minor';
|
||||
|
||||
@Injectable()
|
||||
export class CreateDepositService {
|
||||
private create$ = new Subject<void>();
|
||||
private errorSubject$ = new Subject<boolean>();
|
||||
private pollingErrorSubject$ = new Subject<boolean>();
|
||||
private pollingTimeoutSubject$ = new Subject<boolean>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering, @typescript-eslint/no-unsafe-assignment
|
||||
depositCreated$: Observable<StatDeposit> = this.create$.pipe(
|
||||
map(() => this.getParams()),
|
||||
switchMap((params) =>
|
||||
forkJoin([
|
||||
of(this.getPollingParams(params)),
|
||||
this.fistfulAdminService.CreateDeposit(params).pipe(
|
||||
catchError(() => {
|
||||
this.errorSubject$.next(true);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
]),
|
||||
),
|
||||
switchMap(([pollingParams]) =>
|
||||
this.fistfulStatisticsService.GetDeposits(pollingParams).pipe(
|
||||
catchError(() => {
|
||||
this.pollingErrorSubject$.next(true);
|
||||
return EMPTY;
|
||||
}),
|
||||
map((res) => res.data?.deposits[0]),
|
||||
poll(createDepositStopPollingCondition),
|
||||
catchError(() => {
|
||||
this.pollingTimeoutSubject$.next(true);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
isLoading$ = progress(
|
||||
this.create$,
|
||||
merge([this.depositCreated$, this.errorSubject$, this.pollingErrorSubject$]),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
error$ = this.errorSubject$.asObservable();
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
pollingError$ = this.pollingErrorSubject$.asObservable();
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
pollingTimeout$ = this.pollingTimeoutSubject$.asObservable();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/member-ordering
|
||||
form = this.fb.group({
|
||||
destination: ['', Validators.required],
|
||||
amount: [
|
||||
null as number,
|
||||
[Validators.required, Validators.pattern(/^-?\d+([,.]\d{1,2})?$/)],
|
||||
],
|
||||
currency: [null as StatSource, Validators.required],
|
||||
});
|
||||
|
||||
constructor(
|
||||
private fistfulAdminService: FistfulAdminService,
|
||||
private fistfulStatisticsService: FistfulStatisticsService,
|
||||
private fb: FormBuilder,
|
||||
private idGenerator: UserInfoBasedIdGeneratorService,
|
||||
) {}
|
||||
|
||||
createDeposit() {
|
||||
this.create$.next();
|
||||
}
|
||||
|
||||
private getParams(): DepositParams {
|
||||
const { destination, amount, currency } = this.form.value;
|
||||
return {
|
||||
id: this.idGenerator.getUsernameBasedId(),
|
||||
source: currency.id,
|
||||
destination,
|
||||
body: {
|
||||
amount: toMinor(amount),
|
||||
currency: {
|
||||
symbolic_code: currency.currency_symbolic_code,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private getPollingParams(params: DepositParams): StatRequest {
|
||||
return {
|
||||
dsl: createDsl({
|
||||
deposits: {
|
||||
from_time: getNoTimeZoneIsoString(startOfDay(new Date())),
|
||||
to_time: getNoTimeZoneIsoString(endOfDay(new Date())),
|
||||
size: 1,
|
||||
deposit_id: params.id,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,19 +1,24 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { StatSource } from '@vality/fistful-proto/internal/fistful_stat';
|
||||
import { compareDifferentTypes, NotifyLogService } from '@vality/ng-core';
|
||||
import { Observable, switchMap, of, BehaviorSubject } from 'rxjs';
|
||||
import { shareReplay, map, catchError } from 'rxjs/operators';
|
||||
|
||||
import { FistfulStatisticsService, createDsl } from '@cc/app/api/fistful-stat';
|
||||
import { progressTo } from '@cc/utils';
|
||||
|
||||
import { NotificationErrorService } from '../../shared/services/notification-error';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class FetchSourcesService {
|
||||
sources$: Observable<StatSource[]> = this.fetch().pipe(
|
||||
map((s) => s.sort((a, b) => +new Date(a.created_at) - +new Date(b.created_at))),
|
||||
map((s) =>
|
||||
s.sort(
|
||||
(a, b) =>
|
||||
compareDifferentTypes(a.name, b.name) ||
|
||||
+new Date(a.created_at) - +new Date(b.created_at),
|
||||
),
|
||||
),
|
||||
progressTo(() => this.progress$),
|
||||
shareReplay(1),
|
||||
);
|
||||
@ -21,7 +26,7 @@ export class FetchSourcesService {
|
||||
|
||||
constructor(
|
||||
private fistfulStatisticsService: FistfulStatisticsService,
|
||||
private errorService: NotificationErrorService,
|
||||
private log: NotifyLogService,
|
||||
) {}
|
||||
|
||||
private fetch(
|
||||
@ -35,7 +40,7 @@ export class FetchSourcesService {
|
||||
})
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.errorService.error(err);
|
||||
this.log.error(err);
|
||||
return of(null);
|
||||
}),
|
||||
switchMap((res) =>
|
||||
|
@ -1,7 +1,9 @@
|
||||
<cc-metadata-form
|
||||
[extensions]="extensions"
|
||||
<cc-thrift-editor
|
||||
[defaultValue]="defaultValue"
|
||||
[extensions]="extensions$ | async"
|
||||
[formControl]="control"
|
||||
[metadata]="metadata$ | async"
|
||||
[namespace]="namespace"
|
||||
[namespace]="namespace ?? defaultNamespace"
|
||||
[noChangeKind]="noChangeKind"
|
||||
[type]="type"
|
||||
></cc-metadata-form>
|
||||
></cc-thrift-editor>
|
||||
|
@ -1,31 +1,29 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { ThriftAstMetadata } from '@vality/fistful-proto';
|
||||
import { FormControlSuperclass, createControlProviders } from '@vality/ng-core';
|
||||
import { from, of } from 'rxjs';
|
||||
import { createControlProviders, getImportValue } from '@vality/ng-core';
|
||||
import { of } from 'rxjs';
|
||||
import short from 'short-uuid';
|
||||
|
||||
import { MetadataFormModule, isTypeWithAliases, MetadataFormExtension } from '../metadata-form';
|
||||
import { isTypeWithAliases, MetadataFormExtension } from '../metadata-form';
|
||||
import { BaseThriftFormSuperclass } from '../thrift-api-crud/thrift-forms/utils/thrift-form-superclass';
|
||||
import { ThriftEditorModule } from '../thrift-editor';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cc-fistful-thrift-form',
|
||||
templateUrl: './fistful-thrift-form.component.html',
|
||||
providers: createControlProviders(() => FistfulThriftFormComponent),
|
||||
imports: [CommonModule, ReactiveFormsModule, MetadataFormModule],
|
||||
imports: [CommonModule, ReactiveFormsModule, ThriftEditorModule],
|
||||
})
|
||||
export class FistfulThriftFormComponent extends FormControlSuperclass<unknown> {
|
||||
@Input() namespace: string;
|
||||
@Input() type: string;
|
||||
|
||||
metadata$ = from(
|
||||
import('@vality/fistful-proto/metadata.json').then((m) => m.default as ThriftAstMetadata[]),
|
||||
);
|
||||
extensions: MetadataFormExtension[] = [
|
||||
export class FistfulThriftFormComponent extends BaseThriftFormSuperclass {
|
||||
metadata$ = getImportValue<ThriftAstMetadata[]>(import('@vality/fistful-proto/metadata.json'));
|
||||
internalExtensions$ = of<MetadataFormExtension[]>([
|
||||
{
|
||||
determinant: (data) => of(isTypeWithAliases(data, 'SourceID', 'fistful')),
|
||||
extension: () => of({ generate: () => of(short().uuid()), isIdentifier: true }),
|
||||
},
|
||||
];
|
||||
]);
|
||||
defaultNamespace = 'fistful';
|
||||
}
|
||||
|
@ -1,23 +1,35 @@
|
||||
<ng-container [ngSwitch]="(extensionResult$ | async)?.type">
|
||||
<cc-cash-field
|
||||
*ngSwitchCase="'cash'"
|
||||
[formControl]="control"
|
||||
[required]="data.isRequired"
|
||||
label="{{ data.type | fieldLabel: data.field }} (amount)"
|
||||
minor
|
||||
></cc-cash-field>
|
||||
<div *ngSwitchCase="'datetime'" style="display: flex; gap: 4px">
|
||||
<v-datetime-field
|
||||
[formControl]="control"
|
||||
[hint]="aliases"
|
||||
[label]="(extensionResult$ | async)?.label ?? (data.type | fieldLabel: data.field)"
|
||||
style="flex: 1"
|
||||
></v-datetime-field>
|
||||
<button *ngIf="!data.isRequired && control.value" mat-icon-button (click)="clear($event)">
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="generate$ | async" mat-icon-button (click)="generate($event)">
|
||||
<mat-icon>loop</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="(extensionResult$ | async)?.template as template; else typeTemplate">
|
||||
<ng-template
|
||||
[ngTemplateOutlet]="template"
|
||||
[ngTemplateOutletContext]="{ control: control }"
|
||||
></ng-template>
|
||||
</ng-container>
|
||||
<ng-template #typeTemplate>
|
||||
<ng-container [ngSwitch]="(extensionResult$ | async)?.type">
|
||||
<cc-cash-field
|
||||
*ngSwitchCase="'cash'"
|
||||
[formControl]="control"
|
||||
[required]="data.isRequired"
|
||||
label="{{ data.type | fieldLabel: data.field }} (amount)"
|
||||
minor
|
||||
></cc-cash-field>
|
||||
<div *ngSwitchCase="'datetime'" style="display: flex; gap: 4px">
|
||||
<v-datetime-field
|
||||
[formControl]="control"
|
||||
[hint]="aliases"
|
||||
[label]="(extensionResult$ | async)?.label ?? (data.type | fieldLabel: data.field)"
|
||||
style="flex: 1"
|
||||
></v-datetime-field>
|
||||
<button
|
||||
*ngIf="!data.isRequired && control.value"
|
||||
mat-icon-button
|
||||
(click)="clear($event)"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="generate$ | async" mat-icon-button (click)="generate($event)">
|
||||
<mat-icon>loop</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
@ -1,6 +1,9 @@
|
||||
<div *ngIf="!(extensionResult$ | async)?.hidden" [ngSwitch]="data?.typeGroup">
|
||||
<cc-extension-field
|
||||
*ngIf="(extensionResult$ | async)?.type; else defaultFields"
|
||||
*ngIf="
|
||||
(extensionResult$ | async)?.type || (extensionResult$ | async)?.template;
|
||||
else defaultFields
|
||||
"
|
||||
[data]="data"
|
||||
[extensions]="extensions"
|
||||
[formControl]="control"
|
||||
|
@ -0,0 +1,3 @@
|
||||
::ng-deep .cc-metadata-form-hidden {
|
||||
display: none !important;
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import { Component, Input, OnChanges } from '@angular/core';
|
||||
import { Component, Input, OnChanges, HostBinding, OnInit, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Validator } from '@angular/forms';
|
||||
import { ThriftAstMetadata } from '@vality/domain-proto';
|
||||
import { createControlProviders, FormControlSuperclass } from '@vality/ng-core';
|
||||
import { Field, ValueType } from '@vality/thrift-ts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, BehaviorSubject, defer, switchMap } from 'rxjs';
|
||||
import { map, distinctUntilChanged, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { MetadataFormData } from './types/metadata-form-data';
|
||||
import {
|
||||
@ -15,11 +17,12 @@ import {
|
||||
@Component({
|
||||
selector: 'cc-metadata-form',
|
||||
templateUrl: './metadata-form.component.html',
|
||||
styleUrl: `./metadata-form.component.scss`,
|
||||
providers: createControlProviders(() => MetadataFormComponent),
|
||||
})
|
||||
export class MetadataFormComponent<T>
|
||||
extends FormControlSuperclass<T>
|
||||
implements OnChanges, Validator
|
||||
implements OnInit, OnChanges, Validator
|
||||
{
|
||||
@Input() metadata: ThriftAstMetadata[];
|
||||
@Input() namespace: string;
|
||||
@ -28,8 +31,32 @@ export class MetadataFormComponent<T>
|
||||
@Input() parent?: MetadataFormData;
|
||||
@Input() extensions?: MetadataFormExtension[];
|
||||
|
||||
@HostBinding('class.cc-metadata-form-hidden') hidden = false;
|
||||
|
||||
data: MetadataFormData;
|
||||
extensionResult$: Observable<MetadataFormExtensionResult>;
|
||||
extensionResult$: Observable<MetadataFormExtensionResult> = defer(() => this.updated$).pipe(
|
||||
switchMap(() => getFirstDeterminedExtensionsResult(this.extensions, this.data)),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
private updated$ = new BehaviorSubject<void>(undefined);
|
||||
|
||||
constructor(private destroyRef: DestroyRef) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.extensionResult$
|
||||
.pipe(
|
||||
map((res) => res?.hidden),
|
||||
distinctUntilChanged(),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((hidden) => {
|
||||
this.hidden = hidden;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.metadata && this.namespace && this.type) {
|
||||
@ -41,10 +68,7 @@ export class MetadataFormComponent<T>
|
||||
this.field,
|
||||
this.parent,
|
||||
);
|
||||
this.extensionResult$ = getFirstDeterminedExtensionsResult(
|
||||
this.extensions,
|
||||
this.data,
|
||||
);
|
||||
this.updated$.next(undefined);
|
||||
} catch (err) {
|
||||
this.data = undefined;
|
||||
console.warn(err);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { TemplateRef } from '@angular/core';
|
||||
import { ThemePalette } from '@angular/material/core';
|
||||
import { Observable, combineLatest, switchMap, of } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
@ -22,6 +23,7 @@ export interface MetadataFormExtensionResult {
|
||||
type?: 'datetime' | 'cash';
|
||||
converter?: Converter;
|
||||
hidden?: boolean;
|
||||
template?: TemplateRef<unknown>;
|
||||
}
|
||||
|
||||
export interface MetadataFormExtensionOption {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Input, Directive, OnChanges } from '@angular/core';
|
||||
import { Input, Directive, OnChanges, booleanAttribute } from '@angular/core';
|
||||
import { ThriftAstMetadata } from '@vality/fistful-proto';
|
||||
import { FormControlSuperclass, ComponentChanges } from '@vality/ng-core';
|
||||
import { ValueType } from '@vality/thrift-ts';
|
||||
@ -16,6 +16,7 @@ export abstract class BaseThriftFormSuperclass<T = unknown>
|
||||
@Input() namespace?: string;
|
||||
@Input() extensions?: MetadataFormExtension[];
|
||||
@Input() defaultValue?: T;
|
||||
@Input({ transform: booleanAttribute }) noChangeKind = false;
|
||||
|
||||
protected abstract defaultNamespace: string;
|
||||
protected abstract metadata$: Observable<ThriftAstMetadata[]>;
|
||||
|
@ -19,6 +19,7 @@
|
||||
<mat-icon>restart_alt</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!noChangeKind"
|
||||
color="primary"
|
||||
mat-icon-button
|
||||
matTooltip="Show {{ kind === 'form' ? 'form' : 'JSON' }} editor"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter, booleanAttribute } from '@angular/core';
|
||||
import { ValidationErrors, AbstractControl } from '@angular/forms';
|
||||
import { ThriftAstMetadata } from '@vality/domain-proto';
|
||||
import {
|
||||
@ -35,6 +35,7 @@ export class ThriftEditorComponent<T> extends FormControlSuperclass<T> {
|
||||
@Input() namespace: string;
|
||||
@Input() type: string;
|
||||
@Input() extensions: MetadataFormExtension[];
|
||||
@Input({ transform: booleanAttribute }) noChangeKind = false;
|
||||
|
||||
@Output() changeKind = new EventEmitter<EditorKind>();
|
||||
|
||||
|
1
src/components/source-cash-field/index.ts
Normal file
1
src/components/source-cash-field/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './source-cash-field.component';
|
@ -0,0 +1,18 @@
|
||||
<div style="display: flex; gap: 8px">
|
||||
<mat-form-field style="width: 100%">
|
||||
<mat-label>{{ label || 'Amount' }}</mat-label>
|
||||
<span matPrefix>{{ prefix }} </span>
|
||||
<input
|
||||
[formControl]="amountControl"
|
||||
[inputMask]="amountMask$ | async"
|
||||
[required]="required"
|
||||
matInput
|
||||
/>
|
||||
</mat-form-field>
|
||||
<v-select-field
|
||||
[formControl]="sourceControl"
|
||||
[options]="options$ | async"
|
||||
[style.width.px]="200"
|
||||
label="Currency"
|
||||
></v-select-field>
|
||||
</div>
|
153
src/components/source-cash-field/source-cash-field.component.ts
Normal file
153
src/components/source-cash-field/source-cash-field.component.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import { getCurrencySymbol, CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
Inject,
|
||||
LOCALE_ID,
|
||||
OnInit,
|
||||
booleanAttribute,
|
||||
DestroyRef,
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Validator, ValidationErrors, FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { createMask, InputMaskModule } from '@ngneat/input-mask';
|
||||
import { StatSource } from '@vality/fistful-proto/fistful_stat';
|
||||
import {
|
||||
FormComponentSuperclass,
|
||||
createControlProviders,
|
||||
getValueChanges,
|
||||
Option,
|
||||
SelectFieldModule,
|
||||
} from '@vality/ng-core';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map, first, distinctUntilChanged, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { DomainStoreService } from '../../app/api/domain-config';
|
||||
import { FetchSourcesService } from '../../app/sections/sources';
|
||||
|
||||
export interface SourceCash {
|
||||
amount: number;
|
||||
sourceId: StatSource['id'];
|
||||
}
|
||||
|
||||
const GROUP_SEPARATOR = ' ';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'cc-source-cash-field',
|
||||
templateUrl: './source-cash-field.component.html',
|
||||
providers: createControlProviders(() => SourceCashFieldComponent),
|
||||
imports: [
|
||||
MatFormField,
|
||||
ReactiveFormsModule,
|
||||
InputMaskModule,
|
||||
SelectFieldModule,
|
||||
CommonModule,
|
||||
MatInputModule,
|
||||
],
|
||||
})
|
||||
export class SourceCashFieldComponent
|
||||
extends FormComponentSuperclass<SourceCash>
|
||||
implements Validator, OnInit
|
||||
{
|
||||
@Input() label?: string;
|
||||
@Input({ transform: booleanAttribute }) required: boolean = false;
|
||||
|
||||
amountControl = new FormControl<string>(null);
|
||||
sourceControl = new FormControl<StatSource>(null);
|
||||
|
||||
amountMask$ = getValueChanges(this.sourceControl).pipe(
|
||||
distinctUntilChanged(),
|
||||
map(() =>
|
||||
createMask({
|
||||
alias: 'numeric',
|
||||
groupSeparator: GROUP_SEPARATOR,
|
||||
digits: 0,
|
||||
digitsOptional: true,
|
||||
placeholder: '',
|
||||
}),
|
||||
),
|
||||
);
|
||||
options$ = this.fetchSourcesService.sources$.pipe(
|
||||
map((sources): Option<StatSource>[] =>
|
||||
sources.map((s) => ({
|
||||
label: s.currency_symbolic_code,
|
||||
description: s.name,
|
||||
value: s,
|
||||
})),
|
||||
),
|
||||
);
|
||||
amountSource$ = combineLatest([
|
||||
getValueChanges(this.amountControl).pipe(
|
||||
map((amountStr) =>
|
||||
amountStr ? Number(amountStr.replaceAll(GROUP_SEPARATOR, '')) : null,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
getValueChanges(this.sourceControl),
|
||||
this.domainStoreService.getObjects('currency'),
|
||||
]).pipe(
|
||||
map(([amount, source, currencies]) =>
|
||||
!isNil(amount) && source
|
||||
? {
|
||||
amount,
|
||||
source,
|
||||
currency: currencies.find(
|
||||
(c) => c.data.symbolic_code === source.currency_symbolic_code,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
get currencyCode() {
|
||||
return this.sourceControl.value?.currency_symbolic_code;
|
||||
}
|
||||
|
||||
get prefix() {
|
||||
return getCurrencySymbol(this.currencyCode, 'narrow', this._locale);
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) private _locale: string,
|
||||
private destroyRef: DestroyRef,
|
||||
private fetchSourcesService: FetchSourcesService,
|
||||
private domainStoreService: DomainStoreService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.amountSource$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((amountSource) => {
|
||||
this.emitOutgoingValue(
|
||||
amountSource
|
||||
? { amount: amountSource.amount, sourceId: amountSource.source.id }
|
||||
: null,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return !this.amountControl.value || !this.sourceControl.value
|
||||
? { invalidCash: true }
|
||||
: null;
|
||||
}
|
||||
|
||||
handleIncomingValue(value: SourceCash) {
|
||||
this.amountControl.setValue(
|
||||
typeof value?.amount === 'number' ? String(value.amount) : null,
|
||||
);
|
||||
if (value?.sourceId) {
|
||||
this.options$.pipe(first()).subscribe((opts) => {
|
||||
this.sourceControl.setValue(opts.find((o) => o.value.id === value.sourceId).value);
|
||||
});
|
||||
} else {
|
||||
this.sourceControl.setValue(null);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user