IMP-196: New payment page (#345)

This commit is contained in:
Rinat Arsaev 2024-03-28 15:49:20 +07:00 committed by GitHub
parent 002f47d3f8
commit ed8cb4601d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
54 changed files with 412 additions and 704 deletions

8
package-lock.json generated
View File

@ -25,7 +25,7 @@
"@vality/fistful-proto": "2.0.1-6600be9.0",
"@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/ng-core": "17.2.1-pr-57-6aa62d9.0",
"@vality/ng-core": "17.2.1-pr-57-1a93ecb.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0",
@ -6454,9 +6454,9 @@
"integrity": "sha512-BsDy5ejotfTtUlwuoX3kz+PYJ5NSTW6m5ZRGv+p5HaKXSjR7tserPdv0q133Wp4T+sg0ED0Qr9Peqsrn+9XlDQ=="
},
"node_modules/@vality/ng-core": {
"version": "17.2.1-pr-57-6aa62d9.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.2.1-pr-57-6aa62d9.0.tgz",
"integrity": "sha512-WlPp5EdDRL/tvDOuAsMtbd5xXL8hXWdRdcSLEjHn6SS4+Hnwodki6PzThjk/4T8drj/EUbaZSoTyL2J7lbaciw==",
"version": "17.2.1-pr-57-1a93ecb.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-17.2.1-pr-57-1a93ecb.0.tgz",
"integrity": "sha512-X3PXwqZu6Wej0ETv7mqI6VIZrLJ72GnTszkpbmJkEo+MZSdgzFfgyqMglNz8QKZ0ORclszjlG41J4jtBKspvVQ==",
"dependencies": {
"@angular/material-date-fns-adapter": "^17.2.0",
"@ng-matero/extensions": "^17.1.0",

View File

@ -33,7 +33,7 @@
"@vality/fistful-proto": "2.0.1-6600be9.0",
"@vality/machinegun-proto": "1.0.0",
"@vality/magista-proto": "2.0.2-28d11b9.0",
"@vality/ng-core": "17.2.1-pr-57-6aa62d9.0",
"@vality/ng-core": "17.2.1-pr-57-1a93ecb.0",
"@vality/payout-manager-proto": "2.0.1-eb4091a.0",
"@vality/repairer-proto": "2.0.2-07b73e9.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0",

View File

@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { PartyID } from '@vality/domain-proto/domain';
import { ShopID, WalletID } from '@vality/domain-proto/internal/domain';
import { progressTo } from '@vality/ng-core';
import { BehaviorSubject } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { shareReplay, map } from 'rxjs/operators';
import { MemoizeExpiring } from 'typescript-memoize';
import { PartyManagementService } from '@cc/app/api/payment-processing';
@ -21,4 +22,39 @@ export class PartiesStoreService {
.Get(partyId)
.pipe(progressTo(this.progress$), shareReplay({ refCount: true, bufferSize: 1 }));
}
@MemoizeExpiring(5 * 60_000)
getShop(shopId: ShopID, partyId: PartyID) {
return this.get(partyId).pipe(
map((p) => p.shops.get(shopId)),
progressTo(this.progress$),
shareReplay({ refCount: true, bufferSize: 1 }),
);
}
@MemoizeExpiring(5 * 60_000)
getWallet(walletId: WalletID, partyId: PartyID) {
return this.get(partyId).pipe(
map((p) => p.wallets.get(walletId)),
progressTo(this.progress$),
shareReplay({ refCount: true, bufferSize: 1 }),
);
}
@MemoizeExpiring(5 * 60_000)
getContractor(shopId: ShopID, partyId: PartyID) {
return combineLatest([this.get(partyId), this.getShop(shopId, partyId)]).pipe(
map(([party, shop]) => {
const contractorId = party.contracts.get(shop.contract_id)?.contractor_id;
return party.contractors.get(contractorId)?.contractor;
}),
);
}
@MemoizeExpiring(5 * 60_000)
getContract(shopId: ShopID, partyId: PartyID) {
return combineLatest([this.get(partyId), this.getShop(shopId, partyId)]).pipe(
map(([party, shop]) => party.contracts.get(shop.contract_id)),
);
}
}

View File

@ -1,21 +1,19 @@
<cc-page-layout
[progress]="isLoading$ | async"
description="{{ (payment$ | async)?.id }}"
[tags]="tags$ | async"
id="{{
(payment$ | async) ? (payment$ | async)?.invoice_id + '.' + (payment$ | async)?.id : ''
}}"
title="Payment"
>
<ng-container *ngIf="payment$ | async as payment">
<mat-card>
<mat-card-content>
<cc-payment-main-info
[payment]="payment"
[shop]="shop$ | async"
></cc-payment-main-info>
<!-- <cc-json-viewer-->
<!-- [metadata]="metadata$ | async"-->
<!-- [value]="payment"-->
<!-- namespace="magista"-->
<!-- type="StatPayment"-->
<!-- ></cc-json-viewer>-->
<cc-magista-thrift-viewer
[extensions]="extensions$ | async"
[value]="payment"
type="StatPayment"
></cc-magista-thrift-viewer>
</mat-card-content>
</mat-card>

View File

@ -1,13 +1,27 @@
import { ChangeDetectionStrategy, Component, DestroyRef } from '@angular/core';
import { ChangeDetectionStrategy, Component, DestroyRef, Inject, LOCALE_ID } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { ThriftAstMetadata } from '@vality/domain-proto';
import { DialogService, DialogResponseStatus } from '@vality/ng-core';
import { Subject, merge, defer, from } from 'rxjs';
import { InvoicePaymentStatus, InvoicePaymentFlow } from '@vality/domain-proto/internal/domain';
import {
DialogService,
DialogResponseStatus,
getImportValue,
formatCurrency,
Color,
} from '@vality/ng-core';
import startCase from 'lodash-es/startCase';
import { Subject, merge, defer, Observable, of } from 'rxjs';
import { shareReplay, switchMap, map } from 'rxjs/operators';
import { InvoicingService } from '@cc/app/api/payment-processing';
import { getUnionKey, getUnionValue } from '../../../utils';
import { MetadataViewExtension } from '../../shared/components/json-viewer';
import { isTypeWithAliases } from '../../shared/components/metadata-form';
import { DomainMetadataViewExtensionsService } from '../../shared/components/thrift-api-crud/domain/domain-thrift-viewer/services/domain-metadata-view-extensions';
import { AmountCurrencyService } from '../../shared/services';
import { CreateChargebackDialogComponent } from './create-chargeback-dialog/create-chargeback-dialog.component';
import { PaymentDetailsService } from './payment-details.service';
@ -19,7 +33,6 @@ import { PaymentDetailsService } from './payment-details.service';
export class PaymentDetailsComponent {
payment$ = this.paymentDetailsService.payment$;
isLoading$ = this.paymentDetailsService.isLoading$;
shop$ = this.paymentDetailsService.shop$;
chargebacks$ = merge(
this.route.params,
@ -32,8 +45,80 @@ export class PaymentDetailsComponent {
map(({ chargebacks }) => chargebacks),
shareReplay({ refCount: true, bufferSize: 1 }),
);
metadata$ = from(
import('@vality/magista-proto/metadata.json').then((m) => m.default as ThriftAstMetadata[]),
metadata$ = getImportValue<ThriftAstMetadata[]>(import('@vality/magista-proto/metadata.json'));
extensions$: Observable<MetadataViewExtension[]> = this.payment$.pipe(
map((payment): MetadataViewExtension[] => [
this.domainMetadataViewExtensionsService.createShopExtension(payment.owner_id),
{
determinant: (d) => of(isTypeWithAliases(d, 'Amount', 'domain')),
extension: (_, amount: number) =>
this.amountCurrencyService.getCurrency(payment.currency_symbolic_code).pipe(
map((c) => ({
value: formatCurrency(
amount,
c.data.symbolic_code,
'long',
this._locale,
c.data.exponent,
),
})),
),
},
{
determinant: (d) =>
of(
isTypeWithAliases(d, 'InvoicePaymentStatus', 'domain') ||
isTypeWithAliases(d, 'InvoicePaymentFlow', 'magista'),
),
extension: (_, v) => of({ hidden: !Object.keys(getUnionValue(v)).length }),
},
{
determinant: (d) =>
of(
isTypeWithAliases(d?.trueParent, 'StatPayment', 'magista') &&
(d.field.name === 'make_recurrent' ||
d.field.name === 'currency_symbolic_code' ||
isTypeWithAliases(d, 'InvoiceID', 'domain') ||
isTypeWithAliases(d, 'InvoicePaymentID', 'domain')),
),
extension: () => of({ hidden: true }),
},
]),
shareReplay({ refCount: true, bufferSize: 1 }),
);
tags$ = this.payment$.pipe(
map((payment) => [
{
value: startCase(getUnionKey(payment.status)),
color: (
{
captured: 'success',
refunded: 'success',
charged_back: 'success',
pending: 'pending',
processed: 'pending',
cancelled: 'warn',
failed: 'warn',
} as Record<keyof InvoicePaymentStatus, Color>
)[getUnionKey(payment.status)],
},
{
value: startCase(getUnionKey(payment.flow)),
color: (
{
instant: 'success',
hold: 'pending',
} as Record<keyof InvoicePaymentFlow, Color>
)[getUnionKey(payment.flow)],
},
{
value: payment.make_recurrent ? 'Recurrent' : 'Not Recurrent',
color: payment.make_recurrent ? 'success' : 'neutral',
},
{
value: payment.currency_symbolic_code,
},
]),
);
private updateChargebacks$ = new Subject<void>();
@ -44,6 +129,9 @@ export class PaymentDetailsComponent {
private invoicingService: InvoicingService,
private dialogService: DialogService,
private destroyRef: DestroyRef,
private domainMetadataViewExtensionsService: DomainMetadataViewExtensionsService,
private amountCurrencyService: AmountCurrencyService,
@Inject(LOCALE_ID) private _locale: string,
) {}
createChargeback() {

View File

@ -14,13 +14,12 @@ import { HeadlineModule } from '@cc/components/headline';
import { ChargebacksComponent } from '../../shared/components/chargebacks/chargebacks.component';
import { JsonViewerModule } from '../../shared/components/json-viewer';
import { MetadataFormModule } from '../../shared/components/metadata-form';
import { MagistaThriftViewerComponent } from '../../shared/components/thrift-api-crud';
import { ThriftViewerModule } from '../../shared/components/thrift-viewer';
import { CreateChargebackDialogComponent } from './create-chargeback-dialog/create-chargeback-dialog.component';
import { PaymentDetailsRoutingModule } from './payment-details-routing.module';
import { PaymentDetailsComponent } from './payment-details.component';
import { PaymentMainInfoModule } from './payment-main-info';
import { PaymentToolModule } from './payment-main-info/payment-tool';
import { RefundsTableModule } from './refunds-table';
@NgModule({
@ -31,9 +30,7 @@ import { RefundsTableModule } from './refunds-table';
MatCardModule,
DetailsItemModule,
StatusModule,
PaymentToolModule,
MatProgressSpinnerModule,
PaymentMainInfoModule,
MatButtonModule,
MatDialogModule,
ChargebacksComponent,
@ -45,6 +42,7 @@ import { RefundsTableModule } from './refunds-table';
ThriftViewerModule,
JsonViewerModule,
RefundsTableModule,
MagistaThriftViewerComponent,
],
declarations: [PaymentDetailsComponent, CreateChargebackDialogComponent],
})

View File

@ -1,22 +1,14 @@
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute } from '@angular/router';
import { cleanPrimitiveProps } from '@vality/ng-core';
import { combineLatest, of } from 'rxjs';
import { map, pluck, shareReplay, switchMap, tap } from 'rxjs/operators';
import { cleanPrimitiveProps, NotifyLogService, progressTo, inProgressFrom } from '@vality/ng-core';
import { BehaviorSubject } from 'rxjs';
import { map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { MerchantStatisticsService } from '@cc/app/api/magista';
import { PartyManagementService } from '@cc/app/api/payment-processing';
import { progress } from '@cc/app/shared/custom-operators';
@Injectable()
export class PaymentDetailsService {
private partyID$ = this.route.params.pipe(pluck('partyID'), shareReplay(1));
private routeParams$ = this.route.params.pipe(shareReplay(1));
// eslint-disable-next-line @typescript-eslint/member-ordering
payment$ = this.routeParams$.pipe(
payment$ = this.route.params.pipe(
switchMap(({ partyID, invoiceID, paymentID }) =>
this.merchantStatisticsService
.SearchPayments(
@ -36,28 +28,21 @@ export class PaymentDetailsService {
map(({ payments }) => payments[0]),
tap((payment) => {
if (!payment) {
this.snackBar.open('An error occurred when receiving payment', 'OK');
this.log.error('Payment not found');
}
}),
progressTo(this.progress$),
),
),
shareReplay(1),
shareReplay({ refCount: true, bufferSize: 1 }),
);
isLoading$ = inProgressFrom(() => this.progress$, this.payment$);
// eslint-disable-next-line @typescript-eslint/member-ordering
isLoading$ = progress(this.routeParams$, this.payment$).pipe(shareReplay(1));
// eslint-disable-next-line @typescript-eslint/member-ordering
shop$ = this.payment$.pipe(
switchMap((payment) => combineLatest([this.partyID$, of(payment.shop_id)])),
switchMap(([partyID, shopID]) => this.partyManagementService.GetShop(partyID, shopID)),
shareReplay(1),
);
private progress$ = new BehaviorSubject(0);
constructor(
private partyManagementService: PartyManagementService,
private merchantStatisticsService: MerchantStatisticsService,
private route: ActivatedRoute,
private snackBar: MatSnackBar,
private log: NotifyLogService,
) {}
}

View File

@ -1,2 +0,0 @@
export * from './payment-main-info.module';
export * from './payment-main-info.component';

View File

@ -1,47 +0,0 @@
import { Injectable } from '@angular/core';
import { ContractID, PartyID, Party } from '@vality/domain-proto/domain';
import { forkJoin, merge, of, Subject } from 'rxjs';
import { catchError, filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { PartyManagementService } from '@cc/app/api/payment-processing';
import { progress } from '@cc/app/shared/custom-operators';
@Injectable()
export class FetchContractorService {
private getContractor$ = new Subject<{ partyID: PartyID; contractID: ContractID }>();
private hasError$ = new Subject<void>();
// eslint-disable-next-line @typescript-eslint/member-ordering
contractor$ = this.getContractor$.pipe(
switchMap(({ partyID, contractID }) =>
forkJoin([
of(contractID),
this.partyManagementService.Get(partyID).pipe(
catchError(() => {
this.hasError$.next();
return of('error');
}),
filter((result) => result !== 'error'),
),
]),
),
map(([contractID, party]: [ContractID, Party]) => {
const contractorID = party.contracts.get(contractID)?.contractor_id;
return party.contractors.get(contractorID)?.contractor;
}),
shareReplay(1),
);
// eslint-disable-next-line @typescript-eslint/member-ordering
inProgress$ = progress(this.getContractor$, merge(this.contractor$, this.hasError$)).pipe(
startWith(true),
);
constructor(private partyManagementService: PartyManagementService) {
this.contractor$.subscribe();
}
getContractor(params: { partyID: PartyID; contractID: ContractID }) {
this.getContractor$.next(params);
}
}

View File

@ -1 +0,0 @@
export * from './payment-contractor.module';

View File

@ -1,15 +0,0 @@
<div *ngIf="contractor$ | async as contractor; else empty" style="display: flex; gap: 16px">
<cc-details-item style="flex: 1" title="INN">{{ getINN(contractor) }}</cc-details-item>
<cc-details-item style="flex: 1" title="Registered Name">{{
getRegName(contractor)
}}</cc-details-item>
<div style="flex: 1"></div>
</div>
<ng-template #empty>
<div *ngIf="inProgress$ | async; else emptyResult" style="display: flex">
<cc-details-item style="flex: 1">Loading...</cc-details-item>
</div>
<ng-template #emptyResult>
<div>Error contractor loading.</div>
</ng-template>
</ng-template>

View File

@ -1,41 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
import { ContractID, Contractor, PartyID } from '@vality/domain-proto/domain';
import { FetchContractorService } from './fetch-contractor.service';
@Component({
selector: 'cc-payment-contractor',
templateUrl: 'payment-contractor.component.html',
providers: [FetchContractorService],
})
export class PaymentContractorComponent implements OnInit {
@Input()
contractID: ContractID;
@Input()
partyID: PartyID;
contractor$ = this.fetchContractorService.contractor$;
inProgress$ = this.fetchContractorService.inProgress$;
constructor(private fetchContractorService: FetchContractorService) {}
ngOnInit() {
this.fetchContractorService.getContractor({
partyID: this.partyID,
contractID: this.contractID,
});
}
getINN(contractor: Contractor) {
return contractor.legal_entity?.russian_legal_entity?.inn || '-';
}
getRegName(contractor: Contractor) {
return (
contractor.legal_entity?.russian_legal_entity?.registered_name ||
contractor.legal_entity?.international_legal_entity?.trading_name ||
'-'
);
}
}

View File

@ -1,13 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { DetailsItemModule } from '@cc/components/details-item';
import { PaymentContractorComponent } from './payment-contractor.component';
@NgModule({
declarations: [PaymentContractorComponent],
imports: [DetailsItemModule, CommonModule],
exports: [PaymentContractorComponent],
})
export class PaymentContractorModule {}

View File

@ -1 +0,0 @@
export * from './payment-error.module';

View File

@ -1,15 +0,0 @@
<div
*ngIf="error.code !== 'operation_timeout'; else timeout"
style="display: flex; flex-direction: column; gap: 16px"
>
<div style="display: flex; gap: 16px">
<cc-details-item style="flex: 1" title="Code">{{ error.code }}</cc-details-item>
<cc-details-item style="flex: 2" title="Reason">{{ error.reason }}</cc-details-item>
</div>
<cc-details-item *ngIf="error.path" style="flex: 1" title="Sub Failure">{{
error.path
}}</cc-details-item>
</div>
<ng-template #timeout>
<cc-details-item style="flex: 1">Operation timeout</cc-details-item>
</ng-template>

View File

@ -1,59 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import {
FailureCode,
FailureReason,
SubFailure,
InvoicePaymentStatus,
} from '@vality/domain-proto/domain';
import { getUnionKey } from '@cc/utils/get-union-key';
export interface PaymentError {
code: FailureCode;
reason?: FailureReason;
path?: string;
}
@Component({
selector: 'cc-payment-error',
templateUrl: 'payment-error.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaymentErrorComponent {
@Input()
set status(status: InvoicePaymentStatus) {
const {
failed: { failure },
} = status;
switch (getUnionKey(failure)) {
case 'failure': {
const { code, reason, sub } = failure.failure;
this.error = {
code,
reason,
path: this.makePath(sub),
};
break;
}
case 'operation_timeout':
this.error = {
code: 'operation_timeout',
};
break;
}
}
error: PaymentError;
private makePath(sub: SubFailure): string {
const path = [];
if (sub) {
path.push(sub.code);
if (sub.sub) {
path.push(this.makePath(sub.sub));
}
}
return path.join(' → ');
}
}

View File

@ -1,14 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { DetailsItemModule } from '@cc/components/details-item';
import { PaymentErrorComponent } from './payment-error.component';
@NgModule({
declarations: [PaymentErrorComponent],
imports: [DetailsItemModule, CommonModule, MatProgressSpinnerModule],
exports: [PaymentErrorComponent],
})
export class PaymentErrorModule {}

View File

@ -1,70 +0,0 @@
<div style="display: flex; flex-direction: column; gap: 16px">
<div style="display: flex; gap: 16px">
<cc-details-item style="flex: 1" title="Amount"
>{{ payment.amount | ccFormatAmount }}
{{ payment.currency_symbolic_code | ccCurrency }}</cc-details-item
>
<cc-details-item style="flex: 1" title="Created At">{{
payment.created_at | date: 'dd.MM.yyyy HH:mm:ss'
}}</cc-details-item>
<cc-details-item style="flex: 1" title="Status">
<cc-status [color]="payment.status | toStatus | toPaymentColor">{{
payment.status | toStatus
}}</cc-status>
</cc-details-item>
</div>
<div style="display: flex; gap: 16px">
<cc-details-item style="flex: 1" title="Payer">{{
getPayerEmail(payment.payer)
}}</cc-details-item>
<cc-details-item style="flex: 1" title="Payment Tool">
<cc-payment-tool [paymentTool]="getPaymentTool(payment.payer)"></cc-payment-tool>
</cc-details-item>
<cc-details-item style="flex: 1" title="Invoice">#{{ payment.invoice_id }}</cc-details-item>
</div>
<div style="display: flex; flex-direction: column; gap: 16px">
<div style="display: flex; flex-direction: column; gap: 16px">
<div>
<mat-divider></mat-divider>
</div>
<div class="mat-h1">Shop</div>
<cc-shop-details [shop]="shop"></cc-shop-details>
</div>
<div
*ngIf="hasError(payment.status)"
style="display: flex; flex-direction: column; gap: 16px"
>
<div>
<mat-divider></mat-divider>
</div>
<div class="mat-headline-4 mat-no-margin">Failure</div>
<cc-payment-error [status]="payment.status"></cc-payment-error>
</div>
<div style="display: flex; flex-direction: column; gap: 16px">
<div>
<mat-divider></mat-divider>
</div>
<div class="mat-h1">Terminal</div>
<cc-payment-terminal [terminalID]="payment.terminal_id.id"></cc-payment-terminal>
</div>
<div style="display: flex; flex-direction: column; gap: 16px">
<div>
<mat-divider></mat-divider>
</div>
<div class="mat-h1">Provider</div>
<cc-payment-provider [providerID]="payment.provider_id.id"></cc-payment-provider>
</div>
<div style="display: flex; flex-direction: column; gap: 16px">
<div>
<mat-divider></mat-divider>
</div>
<div class="mat-h1">Contractor</div>
<cc-payment-contractor
*ngIf="payment && shop"
[contractID]="shop.contract_id"
[partyID]="payment?.owner_id"
>
</cc-payment-contractor>
</div>
</div>
</div>

View File

@ -1,40 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Shop, InvoicePaymentStatus, PaymentTool } from '@vality/domain-proto/domain';
import { Payer, StatPayment } from '@vality/magista-proto/magista';
import { getUnionKey } from '../../../../utils';
@Component({
selector: 'cc-payment-main-info',
templateUrl: 'payment-main-info.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaymentMainInfoComponent {
@Input() payment: StatPayment;
@Input() shop: Shop;
getPayerEmail(payer: Payer): string {
if (payer.customer) {
return payer.customer.contact_info.email;
}
if (payer.payment_resource) {
return payer.payment_resource.contact_info.email;
}
if (payer.recurrent) {
return payer.recurrent.contact_info.email;
}
return undefined;
}
getPaymentTool(payer: Payer): PaymentTool {
return (
payer?.customer?.payment_tool ||
payer?.payment_resource?.resource?.payment_tool ||
payer?.recurrent?.payment_tool
);
}
hasError(status: InvoicePaymentStatus): boolean {
return getUnionKey(status) === 'failed';
}
}

View File

@ -1,36 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { ShopDetailsModule, StatusModule } from '@cc/app/shared/components';
import { CommonPipesModule, ThriftPipesModule } from '@cc/app/shared/pipes';
import { DetailsItemModule } from '@cc/components/details-item';
import { PaymentContractorModule } from './payment-contractor';
import { PaymentErrorModule } from './payment-error';
import { PaymentMainInfoComponent } from './payment-main-info.component';
import { PaymentProviderModule } from './payment-provider';
import { PaymentTerminalModule } from './payment-terminal';
import { PaymentToolModule } from './payment-tool';
@NgModule({
imports: [
CommonModule,
MatIconModule,
DetailsItemModule,
StatusModule,
PaymentToolModule,
ThriftPipesModule,
CommonPipesModule,
MatDividerModule,
PaymentContractorModule,
PaymentTerminalModule,
PaymentProviderModule,
PaymentErrorModule,
ShopDetailsModule,
],
declarations: [PaymentMainInfoComponent],
exports: [PaymentMainInfoComponent],
})
export class PaymentMainInfoModule {}

View File

@ -1,40 +0,0 @@
import { Injectable } from '@angular/core';
import { merge, of, Subject } from 'rxjs';
import { catchError, filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
import { progress } from '@cc/app/shared/custom-operators';
@Injectable()
export class FetchProviderService {
private getProvider$ = new Subject<number>();
private hasError$ = new Subject<void>();
// eslint-disable-next-line @typescript-eslint/member-ordering
provider$ = this.getProvider$.pipe(
switchMap((providerID) =>
this.domainStoreService.getObjects('provider').pipe(
map((providerObject) => providerObject.find((obj) => obj.ref.id === providerID)),
catchError(() => {
this.hasError$.next();
return of('error');
}),
filter((result) => result !== 'error'),
),
),
shareReplay(1),
);
// eslint-disable-next-line @typescript-eslint/member-ordering
inProgress$ = progress(this.getProvider$, merge(this.provider$, this.hasError$)).pipe(
startWith(true),
);
constructor(private domainStoreService: DomainStoreService) {
this.provider$.subscribe();
}
getProvider(providerID: number) {
this.getProvider$.next(providerID);
}
}

View File

@ -1 +0,0 @@
export * from './payment-provider.module';

View File

@ -1,13 +0,0 @@
<div *ngIf="provider$ | async as provider; else empty" style="display: flex; gap: 16px">
<cc-details-item style="flex: 1" title="ID">{{ provider.ref.id }}</cc-details-item>
<cc-details-item style="flex: 1" title="Name">{{ provider.data.name }}</cc-details-item>
<div style="flex: 1"></div>
</div>
<ng-template #empty>
<div *ngIf="inProgress$ | async; else emptyResult" style="display: flex">
<cc-details-item style="flex: 1">Loading...</cc-details-item>
</div>
<ng-template #emptyResult>
<div>Error provider loading.</div>
</ng-template>
</ng-template>

View File

@ -1,23 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FetchProviderService } from './fetch-provider.service';
@Component({
selector: 'cc-payment-provider',
templateUrl: 'payment-provider.component.html',
providers: [FetchProviderService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaymentProviderComponent implements OnInit {
@Input()
providerID: number;
provider$ = this.fetchProviderService.provider$;
inProgress$ = this.fetchProviderService.inProgress$;
constructor(private fetchProviderService: FetchProviderService) {}
ngOnInit() {
this.fetchProviderService.getProvider(this.providerID);
}
}

View File

@ -1,13 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { DetailsItemModule } from '@cc/components/details-item';
import { PaymentProviderComponent } from './payment-provider.component';
@NgModule({
declarations: [PaymentProviderComponent],
imports: [DetailsItemModule, CommonModule],
exports: [PaymentProviderComponent],
})
export class PaymentProviderModule {}

View File

@ -1,40 +0,0 @@
import { Injectable } from '@angular/core';
import { merge, of, Subject } from 'rxjs';
import { catchError, filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/domain-config';
import { progress } from '@cc/app/shared/custom-operators';
@Injectable()
export class FetchTerminalService {
private getTerminal$ = new Subject<number>();
private hasError$ = new Subject<void>();
// eslint-disable-next-line @typescript-eslint/member-ordering
terminal$ = this.getTerminal$.pipe(
switchMap((terminalID) =>
this.domainStoreService.getObjects('terminal').pipe(
map((terminalObject) => terminalObject.find((obj) => obj.ref.id === terminalID)),
catchError(() => {
this.hasError$.next();
return of('error');
}),
filter((result) => result !== 'error'),
),
),
shareReplay(1),
);
// eslint-disable-next-line @typescript-eslint/member-ordering
inProgress$ = progress(this.getTerminal$, merge(this.terminal$, this.hasError$)).pipe(
startWith(true),
);
constructor(private domainStoreService: DomainStoreService) {
this.terminal$.subscribe();
}
getTerminal(terminalID: number) {
this.getTerminal$.next(terminalID);
}
}

View File

@ -1 +0,0 @@
export * from './payment-terminal.module';

View File

@ -1,13 +0,0 @@
<div *ngIf="terminal$ | async as terminal; else empty" style="display: flex; gap: 16px">
<cc-details-item style="flex: 1" title="ID">{{ terminal.ref.id }}</cc-details-item>
<cc-details-item style="flex: 1" title="Name">{{ terminal.data.name }}</cc-details-item>
<div style="flex: 1"></div>
</div>
<ng-template #empty>
<div *ngIf="inProgress$ | async; else emptyResult" style="display: flex">
<cc-details-item style="flex: 1">Loading...</cc-details-item>
</div>
<ng-template #emptyResult>
<cc-details-item style="flex: 1">Error terminal loading.</cc-details-item>
</ng-template>
</ng-template>

View File

@ -1,23 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { FetchTerminalService } from './fetch-terminal.service';
@Component({
selector: 'cc-payment-terminal',
templateUrl: 'payment-terminal.component.html',
providers: [FetchTerminalService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaymentTerminalComponent implements OnInit {
@Input()
terminalID: number;
terminal$ = this.fetchTerminalService.terminal$;
inProgress$ = this.fetchTerminalService.inProgress$;
constructor(private fetchTerminalService: FetchTerminalService) {}
ngOnInit() {
this.fetchTerminalService.getTerminal(this.terminalID);
}
}

View File

@ -1,13 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { DetailsItemModule } from '@cc/components/details-item';
import { PaymentTerminalComponent } from './payment-terminal.component';
@NgModule({
declarations: [PaymentTerminalComponent],
imports: [DetailsItemModule, CommonModule],
exports: [PaymentTerminalComponent],
})
export class PaymentTerminalModule {}

View File

@ -1 +0,0 @@
{{ bankCard | toCardNumber }} ({{ bankCard.payment_system.id }})

View File

@ -1,11 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { BankCard } from '@vality/domain-proto/domain';
@Component({
selector: 'cc-bank-card',
templateUrl: 'bank-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BankCardComponent {
@Input() bankCard: BankCard;
}

View File

@ -1,12 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { BankCardComponent } from './bank-card.component';
import { ToCardNumberPipe } from './to-card-number.pipe';
@NgModule({
imports: [CommonModule, MatIconModule],
declarations: [BankCardComponent, ToCardNumberPipe],
exports: [BankCardComponent],
})
export class BankCardModule {}

View File

@ -1,2 +0,0 @@
export * from './bank-card.module';
export * from './bank-card.component';

View File

@ -1,14 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { BankCard } from '@vality/domain-proto/domain';
@Pipe({
name: 'toCardNumber',
})
export class ToCardNumberPipe implements PipeTransform {
transform(card: BankCard): string {
return toCardNumber(card);
}
}
export const toCardNumber = (card: BankCard): string =>
`${card.bin}******${card.last_digits}`.replace(/(.{4})/g, '$& ');

View File

@ -1,2 +0,0 @@
export * from './payment-tool.module';
export * from './payment-tool.component';

View File

@ -1,13 +0,0 @@
<cc-bank-card *ngIf="paymentTool.bank_card" [bankCard]="paymentTool.bank_card"></cc-bank-card>
<div *ngIf="paymentTool.crypto_currency">
{{ paymentTool.crypto_currency?.id }}
</div>
<div *ngIf="paymentTool.digital_wallet">
{{ paymentTool.digital_wallet?.id }}
</div>
<div *ngIf="paymentTool.mobile_commerce">
{{ paymentTool.mobile_commerce.phone }}
</div>
<div *ngIf="paymentTool.payment_terminal">
{{ paymentTool.payment_terminal?.payment_service?.id }}
</div>

View File

@ -1,11 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { PaymentTool } from '@vality/domain-proto/domain';
@Component({
selector: 'cc-payment-tool',
templateUrl: 'payment-tool.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PaymentToolComponent {
@Input() paymentTool: PaymentTool;
}

View File

@ -1,11 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BankCardModule } from './bank-card';
import { PaymentToolComponent } from './payment-tool.component';
@NgModule({
imports: [CommonModule, BankCardModule],
declarations: [PaymentToolComponent],
exports: [PaymentToolComponent],
})
export class PaymentToolModule {}

View File

@ -27,8 +27,8 @@ export class PaymentsTableComponent {
@Output() more = new EventEmitter<void>();
columns: Column<StatPayment>[] = [
{ field: 'id', click: (d) => this.toDetails(d) },
{ field: 'invoice_id' },
{ field: 'id', click: (d) => this.toDetails(d), pinned: 'left' },
{ field: 'invoice_id', pinned: 'left' },
{
field: 'amount',
type: 'currency',
@ -70,6 +70,7 @@ export class PaymentsTableComponent {
'domain_revision',
createTerminalColumn((d) => d.terminal_id.id),
createProviderColumn((d) => d.provider_id.id),
'external_id',
createFailureColumn<StatPayment>(
(d) => d.status?.failed?.failure?.failure,
(d) =>

View File

@ -1,3 +1,10 @@
<mat-toolbar *ngIf="tags?.length" style="height: 48px; padding: 0 24px">
<div style="display: flex; gap: 8px; align-items: center">
<v-tag *ngFor="let tag of tags" [color]="tag.color" style="margin-top: 8px">{{
tag.value
}}</v-tag>
</div>
</mat-toolbar>
<div [ngClass]="{ wrapper__offset: !noOffset }" class="wrapper">
<div *ngIf="title" style="display: flex; flex-direction: column; gap: 8px">
<div *ngIf="(path$ | async)?.length > 1" class="mat-caption mat-secondary-text">

View File

@ -10,7 +10,7 @@ import {
EventEmitter,
} from '@angular/core';
import { Router } from '@angular/router';
import { UrlService } from '@vality/ng-core';
import { UrlService, Color } from '@vality/ng-core';
import { map } from 'rxjs/operators';
@Component({
@ -25,6 +25,7 @@ export class PageLayoutComponent {
@Input() id?: string;
@Input() progress?: boolean;
@Input({ transform: booleanAttribute }) noOffset = false;
@Input() tags?: { value: string; color: Color }[] | null;
@Output() idLinkClick = new EventEmitter<MouseEvent>();

View File

@ -3,9 +3,12 @@ import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatToolbar } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterLink } from '@angular/router';
import { ActionsModule } from '@vality/ng-core';
import { ActionsModule, TagModule } from '@vality/ng-core';
import { ThriftPipesModule } from '../../pipes';
import { PageLayoutActionsComponent } from './components/page-layout-actions/page-layout-actions.component';
import { PageLayoutComponent } from './page-layout.component';
@ -19,6 +22,9 @@ import { PageLayoutComponent } from './page-layout.component';
MatButtonModule,
MatTooltipModule,
ActionsModule,
MatToolbar,
TagModule,
ThriftPipesModule,
],
declarations: [PageLayoutComponent, PageLayoutActionsComponent],
exports: [PageLayoutComponent, PageLayoutActionsComponent],

View File

@ -1,7 +1,31 @@
<cc-card [title]="(shop$ | async)?.details?.name">
<mat-tab-group>
<mat-tab label="Shop">
<div style="padding-top: 24px">
<cc-domain-thrift-viewer
[progress]="!!(progress$ | async)"
[value]="shop$ | async"
type="Shop"
></cc-domain-thrift-viewer>
</div>
</mat-tab>
<mat-tab label="Contract">
<div style="padding-top: 24px">
<cc-domain-thrift-viewer
[progress]="!!(progress$ | async)"
[value]="contract$ | async"
type="Contract"
></cc-domain-thrift-viewer>
</div>
</mat-tab>
<mat-tab label="Contractor">
<div style="padding-top: 24px">
<cc-domain-thrift-viewer
[progress]="!!(progress$ | async)"
[value]="contractor$ | async"
type="Contractor"
></cc-domain-thrift-viewer>
</div>
</mat-tab>
</mat-tab-group>
</cc-card>

View File

@ -1,41 +1,52 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnChanges } from '@angular/core';
import { ComponentChanges } from '@vality/ng-core';
import { ReplaySubject, defer, combineLatest } from 'rxjs';
import { switchMap, map, shareReplay } from 'rxjs/operators';
import { Component, input } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatDivider } from '@angular/material/divider';
import { MatTabGroup, MatTab } from '@angular/material/tabs';
import { PartyID, ShopID } from '@vality/domain-proto/internal/domain';
import { combineLatest } from 'rxjs';
import { switchMap, shareReplay } from 'rxjs/operators';
import { PartiesStoreService } from '../../../api/payment-processing';
import { CardComponent } from '../sidenav-info/components/card/card.component';
import { DomainThriftViewerComponent } from '../thrift-api-crud';
import { DomainThriftViewerComponent, MagistaThriftViewerComponent } from '../thrift-api-crud';
import { ThriftViewerModule } from '../thrift-viewer';
@Component({
selector: 'cc-shop-card',
standalone: true,
imports: [CommonModule, CardComponent, DomainThriftViewerComponent],
imports: [
CommonModule,
CardComponent,
DomainThriftViewerComponent,
MatCard,
MatCardContent,
ThriftViewerModule,
MatDivider,
MagistaThriftViewerComponent,
MatTabGroup,
MatTab,
],
templateUrl: './shop-card.component.html',
})
export class ShopCardComponent implements OnChanges {
@Input() partyId: string;
@Input() id: string;
export class ShopCardComponent {
partyId = input.required<PartyID>();
id = input.required<ShopID>();
progress$ = this.partiesStoreService.progress$;
shop$ = defer(() => this.partyId$).pipe(
switchMap((partyID) => combineLatest([this.partiesStoreService.get(partyID), this.id$])),
map(([party, id]) => party.shops.get(id)),
shop$ = combineLatest([toObservable(this.partyId), toObservable(this.id)]).pipe(
switchMap(([partyId, id]) => this.partiesStoreService.getShop(id, partyId)),
shareReplay({ refCount: true, bufferSize: 1 }),
);
contractor$ = combineLatest([toObservable(this.partyId), toObservable(this.id)]).pipe(
switchMap(([partyId, id]) => this.partiesStoreService.getContractor(id, partyId)),
shareReplay({ refCount: true, bufferSize: 1 }),
);
contract$ = combineLatest([toObservable(this.partyId), toObservable(this.id)]).pipe(
switchMap(([partyId, id]) => this.partiesStoreService.getContract(id, partyId)),
shareReplay({ refCount: true, bufferSize: 1 }),
);
private id$ = new ReplaySubject<string>(1);
private partyId$ = new ReplaySubject<string>(1);
constructor(private partiesStoreService: PartiesStoreService) {}
ngOnChanges(changes: ComponentChanges<ShopCardComponent>) {
if (changes.id) {
this.id$.next(this.id);
}
if (changes.partyId) {
this.partyId$.next(this.partyId);
}
}
}

View File

@ -1,4 +1,4 @@
import { Injectable, Type, Inject, Optional } from '@angular/core';
import { Injectable, Type, Inject, Optional, InputSignal } from '@angular/core';
import {
QueryParamsService,
QueryParamsNamespace,
@ -12,6 +12,10 @@ import { filter, map } from 'rxjs/operators';
import { SIDENAV_INFO_COMPONENTS, SidenavInfoComponents } from './tokens';
type InputType<T> = {
[N in keyof T]?: T[N] extends InputSignal<infer S> ? S : T[N];
};
@Injectable({
providedIn: 'root',
})
@ -50,7 +54,7 @@ export class SidenavInfoService {
toggle<C extends Type<unknown>>(
component: PossiblyAsync<C>,
inputs: { [N in keyof InstanceType<C>]?: InstanceType<C>[N] } = {},
inputs: InputType<InstanceType<C>> = {},
) {
getPossiblyAsyncObservable(component).subscribe((comp) => {
if (this.isEqual(comp, inputs)) {
@ -61,10 +65,7 @@ export class SidenavInfoService {
});
}
open<C extends Type<unknown>>(
component: C,
inputs: { [N in keyof InstanceType<C>]?: InstanceType<C>[N] } = {},
) {
open<C extends Type<unknown>>(component: C, inputs: InputType<InstanceType<C>> = {}) {
this.component$.next(component);
this.inputs = inputs;
void this.qp.set({
@ -94,7 +95,7 @@ export class SidenavInfoService {
private isEqual<C extends Type<unknown>>(
component: C,
inputs: { [N in keyof InstanceType<C>]?: InstanceType<C>[N] } = {},
inputs: InputType<InstanceType<C>> = {},
) {
return component === this.component$.value && isEqual(this.inputs, inputs);
}

View File

@ -4,6 +4,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ThriftAstMetadata } from '@vality/domain-proto';
import { DomainObject } from '@vality/domain-proto/domain';
import { Rational, Timestamp } from '@vality/domain-proto/internal/base';
import { PartyID, ShopID } from '@vality/domain-proto/internal/domain';
import { getImportValue } from '@vality/ng-core';
import isEqual from 'lodash-es/isEqual';
import round from 'lodash-es/round';
@ -15,6 +16,7 @@ import { MetadataViewExtension } from '@cc/app/shared/components/json-viewer';
import { isTypeWithAliases, MetadataFormData } from '@cc/app/shared/components/metadata-form';
import { getUnionValue } from '../../../../../../../../utils';
import { PartiesStoreService } from '../../../../../../../api/payment-processing';
import { SidenavInfoService } from '../../../../../sidenav-info';
import { getDomainObjectDetails } from '../../../utils';
@ -27,6 +29,17 @@ export class DomainMetadataViewExtensionsService {
).pipe(
map((metadata): MetadataViewExtension[] => [
...this.createDomainObjectExtensions(metadata),
{
determinant: (data) => of(isTypeWithAliases(data, 'PartyID', 'domain')),
extension: (_, partyId: PartyID) =>
this.partiesStoreService.get(partyId).pipe(
map((p) => ({
value: p.contact_info.email,
link: [[`/party/${p.id}`]],
tooltip: p.id,
})),
),
},
{
determinant: (data) => of(isTypeWithAliases(data, 'Timestamp', 'base')),
extension: (_, value: Timestamp) =>
@ -53,8 +66,33 @@ export class DomainMetadataViewExtensionsService {
private domainStoreService: DomainStoreService,
private sidenavInfoService: SidenavInfoService,
private destroyRef: DestroyRef,
private partiesStoreService: PartiesStoreService,
) {}
createShopExtension(partyId: PartyID): MetadataViewExtension {
return {
determinant: (data) => of(isTypeWithAliases(data, 'ShopID', 'domain')),
extension: (_, shopId: ShopID) =>
this.partiesStoreService.getShop(shopId, partyId).pipe(
map((p) => ({
value: p.details.name,
tooltip: shopId,
click: () => {
this.sidenavInfoService.toggle(
import('../../../../../shop-card/shop-card.component').then(
(r) => r.ShopCardComponent,
),
{
partyId,
id: shopId,
},
);
},
})),
),
};
}
createDomainObjectExtensions(metadata: ThriftAstMetadata[]): MetadataViewExtension[] {
const domainFields = new MetadataFormData<string, 'struct'>(
metadata,

View File

@ -1 +1,2 @@
export * from './magista-thrift-form';
export * from './magista-thrift-viewer.component';

View File

@ -0,0 +1,23 @@
import { Component, inject } from '@angular/core';
import { ThriftAstMetadata } from '@vality/domain-proto';
import { getImportValue } from '@vality/ng-core';
import { Observable } from 'rxjs';
import { MetadataViewExtension } from '../../json-viewer';
import { DomainMetadataViewExtensionsService } from '../domain/domain-thrift-viewer/services/domain-metadata-view-extensions';
import { ThriftViewerSuperclass, ThriftViewerBaseModule } from '../utils';
@Component({
standalone: true,
selector: 'cc-magista-thrift-viewer',
template: `<cc-thrift-viewer-base [data]="data()" />`,
imports: [ThriftViewerBaseModule],
})
export class MagistaThriftViewerComponent<T> extends ThriftViewerSuperclass<T> {
domainMetadataViewExtensionsService = inject(DomainMetadataViewExtensionsService);
defaultNamespace = 'magista';
metadata$ = getImportValue<ThriftAstMetadata[]>(import('@vality/magista-proto/metadata.json'));
extensions$: Observable<MetadataViewExtension[]> =
this.domainMetadataViewExtensionsService.extensions$;
}

View File

@ -0,0 +1 @@
export * from './thrift-viewer-superclass';

View File

@ -0,0 +1,2 @@
export * from './thrift-viewer-superclass.directive';
export * from './thrift-viewer-base.module';

View File

@ -0,0 +1,11 @@
<cc-thrift-viewer
*ngIf="data() as data"
[compared]="data.compared"
[extensions]="data.extensions$ | async"
[kind]="data.kind"
[metadata]="data.metadata$ | async"
[namespace]="data.namespace"
[progress]="data.progress"
[type]="data.type"
[value]="data.value"
></cc-thrift-viewer>

View File

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ThriftViewerModule } from '../../../thrift-viewer';
import { ThriftViewerBaseComponent } from './thrift-viewer-superclass.directive';
@NgModule({
imports: [CommonModule, ThriftViewerModule],
declarations: [ThriftViewerBaseComponent],
exports: [ThriftViewerBaseComponent],
})
export class ThriftViewerBaseModule {}

View File

@ -0,0 +1,74 @@
import {
Component,
Directive,
Input,
booleanAttribute,
input,
signal,
OnChanges,
ChangeDetectionStrategy,
} from '@angular/core';
import { ThriftAstMetadata } from '@vality/domain-proto';
import { ValueType } from '@vality/thrift-ts';
import { Observable, of } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { MetadataViewExtension } from '../../../json-viewer';
import { ViewerKind } from '../../../thrift-viewer';
interface Data<T> {
kind: ViewerKind;
value: T;
compared?: T;
type: ValueType;
progress: boolean;
namespace: string;
metadata$: Observable<ThriftAstMetadata[]>;
extensions$: Observable<MetadataViewExtension[]>;
}
@Component({
selector: 'cc-thrift-viewer-base',
templateUrl: './thrift-viewer-base.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ThriftViewerBaseComponent<T> {
data = input.required<Data<T>>();
}
@Directive()
export abstract class ThriftViewerSuperclass<T> implements OnChanges {
@Input() kind = ViewerKind.Component;
@Input() value: T;
@Input() compared?: T;
@Input() type: ValueType;
@Input({ transform: booleanAttribute }) progress = false;
@Input() extensions: MetadataViewExtension[] = [];
@Input() namespace?: string;
abstract defaultNamespace: string;
abstract metadata$: Observable<ThriftAstMetadata[]>;
extensions$: Observable<MetadataViewExtension[]> = of([]);
data = signal<Data<T>>(this.createData());
ngOnChanges() {
this.data.set(this.createData());
}
private createData() {
return {
kind: this.kind,
value: this.value,
compared: this.compared,
type: this.type,
progress: this.progress,
namespace: this.namespace || this.defaultNamespace,
metadata$: this.metadata$,
extensions$: this.extensions$.pipe(
map((ext) => [...(ext || []), ...(this.extensions || [])]),
shareReplay({ refCount: true, bufferSize: 1 }),
),
};
}
}