IMP-84: Add duplicate shop delegates, new parties, shops table (#273)

This commit is contained in:
Rinat Arsaev 2023-10-13 22:56:59 +07:00 committed by GitHub
parent 908f6de4ac
commit bf8c021c9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 486 additions and 774 deletions

8
package-lock.json generated
View File

@ -29,7 +29,7 @@
"@vality/dominant-cache-proto": "2.0.0",
"@vality/fistful-proto": "2.0.1-ed97a0e.0",
"@vality/magista-proto": "2.0.2-37b81e6.0",
"@vality/ng-core": "16.2.1-pr-40-adb1f52.0",
"@vality/ng-core": "16.2.1-pr-40-bdc62db.0",
"@vality/payout-manager-proto": "2.0.0",
"@vality/repairer-proto": "2.0.2-f5e3b7a.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0",
@ -6000,9 +6000,9 @@
"integrity": "sha512-gJizpTWuB74L+XuJ+dUaxAwJDkycdnuVwrXWIl/NKcS7++/zgrgTpw+tM5/Te3rWqkkCnSxC1SK0C4aPbbtifg=="
},
"node_modules/@vality/ng-core": {
"version": "16.2.1-pr-40-adb1f52.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-16.2.1-pr-40-adb1f52.0.tgz",
"integrity": "sha512-dePHY8wWDE/lDthKkSibBrBQHTAbs1G8nHFiAnAR3Ntt6MOuWcti2qy9i9AxQ9nSN6o7yTzqS8S+GcbaVUYnpA==",
"version": "16.2.1-pr-40-bdc62db.0",
"resolved": "https://registry.npmjs.org/@vality/ng-core/-/ng-core-16.2.1-pr-40-bdc62db.0.tgz",
"integrity": "sha512-hw7ljBUP1X1WYt6ZEQZ2E+y0Udy7G5pUNWL1wUIMmexaXr5H21NTuRZRgy9u3zfxrYfNnLmmI1XZgLfZry/gkA==",
"dependencies": {
"@ng-matero/extensions": "^16.0.0",
"@s-libs/js-core": "^16.0.0",

View File

@ -37,7 +37,7 @@
"@vality/dominant-cache-proto": "2.0.0",
"@vality/fistful-proto": "2.0.1-ed97a0e.0",
"@vality/magista-proto": "2.0.2-37b81e6.0",
"@vality/ng-core": "16.2.1-pr-40-adb1f52.0",
"@vality/ng-core": "16.2.1-pr-40-bdc62db.0",
"@vality/payout-manager-proto": "2.0.0",
"@vality/repairer-proto": "2.0.2-f5e3b7a.0",
"@vality/thrift-ts": "2.4.1-8ad5123.0",

View File

@ -1,4 +1,26 @@
<div fxLayout="column" fxLayoutGap="24px">
<div class="mat-headline-4">Merchant's shops</div>
<cc-shops-table [shops]="shops$ | async"></cc-shops-table>
<div fxLayout="column" fxLayoutGap="24px">
<mat-card>
<mat-card-content>
<v-input-field
[formControl]="filterControl"
fxFlex="100"
label="Filter"
></v-input-field>
</mat-card-content>
</mat-card>
<v-table [columns]="columns" [data]="shops$ | async" noActions></v-table>
</div>
</div>
<ng-template #shopTpl>
<cc-domain-thrift-viewer [value]="selectedShop" type="Shop"></cc-domain-thrift-viewer>
</ng-template>
<ng-template #contractTpl>
<cc-domain-thrift-viewer
[progress]="!!(progress$ | async)"
[value]="getContract(selectedShop.id) | async"
type="Contract"
></cc-domain-thrift-viewer>
</ng-template>

View File

@ -1,4 +1,24 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, TemplateRef, ViewChild } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Shop } from '@vality/domain-proto/domain';
import {
Column,
ConfirmDialogComponent,
createOperationColumn,
DialogResponseStatus,
DialogService,
NotifyLogService,
progressTo,
} from '@vality/ng-core';
import startCase from 'lodash-es/startCase';
import { BehaviorSubject, combineLatest, map, switchMap } from 'rxjs';
import { debounceTime, filter, shareReplay, startWith } from 'rxjs/operators';
import { Memoize } from 'typescript-memoize';
import { PartyManagementService } from '@cc/app/api/payment-processing';
import { SidenavInfoService } from '@cc/app/shared/components/sidenav-info';
import { getUnionKey } from '@cc/utils';
import { PartyShopsService } from './party-shops.service';
@ -8,6 +28,163 @@ import { PartyShopsService } from './party-shops.service';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PartyShopsComponent {
shops$ = this.partyShopsService.shops$;
constructor(private partyShopsService: PartyShopsService) {}
@ViewChild('shopTpl') shopTpl: TemplateRef<unknown>;
@ViewChild('contractTpl') contractTpl: TemplateRef<unknown>;
filterControl = new FormControl('');
shops$ = combineLatest([
this.partyShopsService.shops$,
this.filterControl.valueChanges.pipe(
startWith(this.filterControl.value),
debounceTime(200),
),
]).pipe(
map(([shops, searchStr]) =>
shops.filter((s) => JSON.stringify(s).toLowerCase().includes(searchStr.toLowerCase())),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
selectedShop?: Shop;
columns: Column<Shop>[] = [
{
field: 'details.name',
description: 'id',
pinned: 'left',
click: (d) => {
this.selectedShop = d;
this.sidenavInfoService.toggle(this.shopTpl, d.details.name || `Shop #${d.id}`, d);
},
},
{
field: 'contract_id',
header: 'Contract',
click: (d) => {
this.selectedShop = d;
this.sidenavInfoService.toggle(this.contractTpl, `Contract #${d.id}`, d);
},
},
{
field: 'details.description',
},
{
field: 'location.url',
},
{
field: 'account.currency.symbolic_code',
header: 'Currency',
},
{
field: 'blocking',
type: 'tag',
formatter: (shop) => getUnionKey(shop.blocking),
typeParameters: {
label: (shop) => startCase(getUnionKey(shop.blocking)),
tags: {
blocked: { color: 'warn' },
unblocked: { color: 'success' },
},
},
},
{
field: 'suspension',
type: 'tag',
formatter: (shop) => getUnionKey(shop.suspension),
typeParameters: {
label: (shop) => startCase(getUnionKey(shop.suspension)),
tags: {
suspended: { color: 'warn' },
active: { color: 'success' },
},
},
},
createOperationColumn([
{
label: (shop) =>
getUnionKey(shop.suspension) === 'suspended' ? 'Activate' : 'Suspend',
click: (shop) => {
this.toggleSuspension(shop);
},
},
{
label: (shop) => (getUnionKey(shop.blocking) === 'blocked' ? 'Unblock' : 'Block'),
click: (shop) => {
this.toggleBlocking(shop);
},
},
]),
];
progress$ = new BehaviorSubject(0);
constructor(
private partyShopsService: PartyShopsService,
private route: ActivatedRoute,
private sidenavInfoService: SidenavInfoService,
private partyManagementService: PartyManagementService,
private dialogService: DialogService,
private log: NotifyLogService,
) {}
@Memoize()
getContract(shopID: string) {
return this.partyManagementService
.GetShopContract(this.route.snapshot.params.partyID, shopID)
.pipe(
progressTo(this.progress$),
map((c) => c.contract),
shareReplay({ refCount: true, bufferSize: 1 }),
);
}
toggleBlocking(shop: Shop) {
const partyID = this.route.snapshot.params.partyID;
this.dialogService
.open(ConfirmDialogComponent, {
title: getUnionKey(shop.blocking) === 'unblocked' ? 'Block shop' : 'Unblock shop',
hasReason: true,
})
.afterClosed()
.pipe(
filter((r) => r.status === DialogResponseStatus.Success),
switchMap((r) =>
getUnionKey(shop.blocking) === 'unblocked'
? this.partyManagementService.BlockShop(partyID, shop.id, r.data.reason)
: this.partyManagementService.UnblockShop(partyID, shop.id, r.data.reason),
),
)
.subscribe({
next: () => {
this.partyShopsService.reload();
this.log.success();
},
error: (err) => {
this.log.error(err);
},
});
}
toggleSuspension(shop: Shop) {
const partyID = this.route.snapshot.params.partyID;
this.dialogService
.open(ConfirmDialogComponent, {
title: getUnionKey(shop.suspension) === 'active' ? 'Suspend shop' : 'Activate shop',
})
.afterClosed()
.pipe(
filter((r) => r.status === DialogResponseStatus.Success),
switchMap(() =>
getUnionKey(shop.suspension) === 'active'
? this.partyManagementService.SuspendShop(partyID, shop.id)
: this.partyManagementService.ActivateShop(partyID, shop.id),
),
)
.subscribe({
next: () => {
this.partyShopsService.reload();
this.log.success();
},
error: (err) => {
this.log.error(err);
},
});
}
}

View File

@ -1,33 +1,26 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { ReactiveFormsModule } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { InputFieldModule, TableModule } from '@vality/ng-core';
import { DomainThriftViewerComponent } from '@cc/app/shared/components/thrift-api-crud';
import { PartyShopsRoutingModule } from './party-shops-routing.module';
import { PartyShopsComponent } from './party-shops.component';
import { ShopsTableComponent } from './shops-table/shops-table.component';
@NgModule({
imports: [
PartyShopsRoutingModule,
CommonModule,
FlexModule,
MatPaginatorModule,
MatTableModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatCardModule,
MatIconModule,
MatMenuModule,
TableModule,
ReactiveFormsModule,
InputFieldModule,
DomainThriftViewerComponent,
],
declarations: [PartyShopsComponent, ShopsTableComponent],
declarations: [PartyShopsComponent],
})
export class PartyShopsModule {}

View File

@ -1,20 +1,25 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Party } from '@vality/domain-proto/domain';
import { defer, Observable } from 'rxjs';
import { map, pluck, shareReplay, switchMap } from 'rxjs/operators';
import { defer, merge, Observable, Subject } from 'rxjs';
import { map, shareReplay, switchMap } from 'rxjs/operators';
import { PartyManagementService } from '@cc/app/api/payment-processing';
@Injectable()
export class PartyShopsService {
shops$ = defer(() => this.party$).pipe(
pluck('shops'),
map((p) => p.shops),
map((shops) => Array.from(shops.values())),
);
private party$: Observable<Party> = this.route.params.pipe(
pluck('partyID'),
private reload$ = new Subject<void>();
private party$: Observable<Party> = merge(
this.route.params,
this.reload$.pipe(map(() => this.route.snapshot.params)),
).pipe(
map((p) => p.partyID),
switchMap((partyID) => this.partyManagementService.Get(partyID)),
shareReplay(1),
);
@ -23,4 +28,8 @@ export class PartyShopsService {
private partyManagementService: PartyManagementService,
private route: ActivatedRoute,
) {}
reload() {
this.reload$.next();
}
}

View File

@ -1,51 +0,0 @@
<div fxLayout="column" fxLayoutGap="24px">
<mat-card>
<mat-card-content>
<mat-form-field fxFlex="25">
<input
matInput
placeholder="Filter"
type="text"
(keyup)="applyFilter($event.target.value)"
/>
</mat-form-field>
</mat-card-content>
</mat-card>
<mat-card>
<mat-card-content fxLayout="column">
<table [dataSource]="dataSource" fxFlex mat-table>
<ng-container matColumnDef="id">
<th *matHeaderCellDef mat-header-cell>Shop ID</th>
<td *matCellDef="let shop" mat-cell>{{ shop.id }}</td>
</ng-container>
<ng-container matColumnDef="name">
<th *matHeaderCellDef mat-header-cell>Name</th>
<td *matCellDef="let shop" mat-cell>{{ shop.details.name }}</td>
</ng-container>
<ng-container matColumnDef="url">
<th *matHeaderCellDef mat-header-cell>Url</th>
<td *matCellDef="let shop" mat-cell>{{ shop.location.url }}</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="action-cell" mat-header-cell></th>
<td *matCellDef="let shop" class="action-cell" mat-cell>
<button [matMenuTriggerFor]="menu" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="navigateToShop(shop.id)">Details</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let shop; columns: displayedColumns" mat-row></tr>
</table>
<mat-paginator
[pageSizeOptions]="[10, 20, 50, 100, 250, 500]"
showFirstLastButtons
></mat-paginator>
</mat-card-content>
</mat-card>
</div>

View File

@ -1,11 +0,0 @@
table {
width: 100%;
}
.action-cell {
width: 8px;
}
table {
width: 100%;
}

View File

@ -1,43 +0,0 @@
import { Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { Shop } from '@vality/domain-proto/domain';
import { pluck } from 'rxjs/operators';
@Component({
selector: 'cc-shops-table',
templateUrl: 'shops-table.component.html',
styleUrls: ['shops-table.component.scss'],
})
export class ShopsTableComponent implements OnChanges {
@Input() shops: Shop[];
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
dataSource: MatTableDataSource<Shop> = new MatTableDataSource();
displayedColumns = ['id', 'name', 'url', 'actions'];
constructor(
private router: Router,
private route: ActivatedRoute,
) {}
ngOnChanges({ shops }: SimpleChanges) {
if (shops.currentValue) {
this.dataSource.data = shops.currentValue;
this.dataSource.filterPredicate = (shop: Shop, filter: string) =>
JSON.stringify(shop).toLowerCase().includes(filter);
this.dataSource.paginator = this.paginator;
}
}
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
navigateToShop(shopID: string) {
this.route.params.pipe(pluck('partyID')).subscribe((partyID: string) => {
void this.router.navigate([`/party/${partyID}/shop/${shopID}`]);
});
}
}

View File

@ -20,11 +20,6 @@ import { ROUTING_CONFIG } from './routing-config';
loadChildren: () =>
import('../party-shops').then((m) => m.PartyShopsModule),
},
{
path: 'shop/:shopID',
loadChildren: () =>
import('../shop-details').then((m) => m.ShopDetailsModule),
},
{
path: 'routing-rules',
loadChildren: () =>

View File

@ -1,19 +1,13 @@
<ng-container *ngIf="isLoading$ | async; else loaded">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</ng-container>
<ng-template #loaded>
<div [fxLayoutGap]="(data$ | async)?.length ? '24px' : '64px'" fxLayout="column">
<div fxLayout fxLayoutAlign="space-between center" fxLayoutGap="8px">
<div class="mat-headline-4 mat-no-margin">Party delegate rulesets</div>
<button color="primary" mat-button (click)="attachNewRuleset()">
Attach new ruleset
</button>
</div>
<cc-routing-rules-list
[data]="data$ | async"
[displayedColumns]="displayedColumns"
(toDetails)="navigateToPartyRuleset($event.parentRefId, $event.delegateIdx)"
></cc-routing-rules-list>
<div fxLayout="column" fxLayoutGap="24px">
<div fxLayout fxLayoutAlign="space-between center" fxLayoutGap="8px">
<div class="mat-headline-4 mat-no-margin">Party delegate rulesets</div>
<button color="primary" mat-button (click)="attachNewRuleset()">Attach new ruleset</button>
</div>
</ng-template>
<cc-routing-rules-list
[data]="data$ | async"
[displayedColumns]="displayedColumns"
[progress]="isLoading$ | async"
(toDetails)="navigateToPartyRuleset($event.parentRefId, $event.delegateIdx)"
></cc-routing-rules-list>
</div>

View File

@ -23,9 +23,9 @@ import { PartyDelegateRulesetsService } from './party-delegate-rulesets.service'
})
export class PartyDelegateRulesetsComponent {
displayedColumns = [
{ key: 'partyDelegate', name: 'Party delegate' },
{ key: 'paymentInstitution', name: 'Payment institution' },
{ key: 'mainRuleset', name: 'Main ruleset' },
{ key: 'partyDelegate', name: 'Party delegate' },
];
isLoading$ = this.domainStoreService.isLoading$;
data$ = this.partyDelegateRulesetsService.getDelegatesWithPaymentInstitution().pipe(

View File

@ -5,7 +5,7 @@
<div
*ngIf="partyRuleset$ | async as partyRuleset; else emptyPartyDelegate"
fxLayout="column"
fxLayoutGap="64px"
fxLayoutGap="24px"
>
<cc-routing-ruleset-header
[backTo]="
@ -18,14 +18,14 @@
</cc-routing-ruleset-header>
<cc-routing-rules-list
*ngIf="(shopsData$ | async)?.length || !(walletsData$ | async)?.length"
*ngIf="(routingRulesType$ | async) === 'payment'"
[data]="shopsData$ | async"
[displayedColumns]="shopsDisplayedColumns"
(toDetails)="navigateToDelegate($event.parentRefId, $event.delegateIdx)"
></cc-routing-rules-list>
<cc-routing-rules-list
*ngIf="(walletsData$ | async)?.length"
*ngIf="(routingRulesType$ | async) === 'withdrawal'"
[data]="walletsData$ | async"
[displayedColumns]="walletsDisplayedColumns"
(toDetails)="navigateToDelegate($event.parentRefId, $event.delegateIdx)"

View File

@ -30,14 +30,17 @@ export class PartyRoutingRulesetComponent {
isLoading$ = this.domainStoreService.isLoading$;
shopsDisplayedColumns = [
{ key: 'shop', name: 'Shop' },
{ key: 'id', name: 'Delegate (Ruleset Ref ID)' },
{ key: 'shop', name: 'Shop' },
];
walletsDisplayedColumns = [
{ key: 'wallet', name: 'Wallet' },
{ key: 'id', name: 'Delegate (Ruleset Ref ID)' },
{ key: 'wallet', name: 'Wallet' },
];
shopsData$ = combineLatest([this.partyRuleset$, this.partyRoutingRulesetService.shops$]).pipe(
shopsData$ = combineLatest([
this.partyRuleset$,
this.partyRoutingRulesetService.shops$.pipe(startWith([])),
]).pipe(
filter(([r]) => !!r),
map(([ruleset, shops]) =>
ruleset.data.decisions.delegates
@ -63,7 +66,7 @@ export class PartyRoutingRulesetComponent {
);
walletsData$ = combineLatest([
this.partyRuleset$,
this.partyRoutingRulesetService.wallets$,
this.partyRoutingRulesetService.wallets$.pipe(startWith([])),
]).pipe(
filter(([r]) => !!r),
map(([ruleset, wallets]) =>

View File

@ -1,70 +1 @@
<mat-card *ngIf="(dataSource$ | async)?.data?.length; else empty">
<mat-card-content fxLayout="column">
<table [dataSource]="dataSource$ | async" mat-table>
<ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column.key">
<th *matHeaderCellDef mat-header-cell>{{ column.name }}</th>
<td *matCellDef="let element" mat-cell>
<div
*ngIf="
element[column.key]?.text || element[column.key]?.caption;
else onlyBody
"
fxLayout="column"
>
<div class="mat-body-1">{{ element[column.key]?.text || '&nbsp;' }}</div>
<div class="mat-caption">
{{ element[column.key]?.caption || '&nbsp;' }}
</div>
</div>
<ng-template #onlyBody>
{{ element[column.key] }}
</ng-template>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef mat-header-cell width="1px"></th>
<td *matCellDef="let element" mat-cell>
<button [matMenuTriggerFor]="menu" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button
mat-menu-item
(click)="
toDetails.emit({
parentRefId: element?.parentRefId,
delegateIdx: element?.delegateIdx
})
"
>
Details
</button>
<button mat-menu-item (click)="changeDelegateRuleset(element)">
Change delegate ruleset
</button>
<button mat-menu-item (click)="changeTarget(element)">
Change main ruleset
</button>
<button mat-menu-item (click)="cloneDelegateRuleset(element)">
Clone delegate ruleset
</button>
<button mat-menu-item (click)="delete(element)">Delete</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="allDisplayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: allDisplayedColumns" mat-row></tr>
</table>
<mat-paginator
[pageSizeOptions]="[10, 20, 50, 100, 250, 500]"
showFirstLastButtons
></mat-paginator>
</mat-card-content>
</mat-card>
<ng-template #empty>
<div class="mat-display-1" fxLayout="column" fxLayoutAlign=" center">
Routing rules not found
</div>
</ng-template>
<v-table [columns]="columns" [data]="data" [progress]="progress" noActions></v-table>

View File

@ -1,34 +0,0 @@
import { Component, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RoutingRulesListComponent } from './routing-rules-list.component';
@Component({
selector: 'cc-host',
template: `<dsh-routing-rules-list></dsh-routing-rules-list>`,
})
class HostComponent {}
describe('RoutingRulesListComponent', () => {
let fixture: ComponentFixture<HostComponent>;
let debugElement: DebugElement;
let component: RoutingRulesListComponent;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [],
declarations: [HostComponent, RoutingRulesListComponent],
}).compileComponents();
fixture = TestBed.createComponent(HostComponent);
debugElement = fixture.debugElement.query(By.directive(RoutingRulesListComponent));
component = debugElement.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@ -4,15 +4,20 @@ import {
EventEmitter,
Input,
Output,
ViewChild,
OnChanges,
} from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { DialogResponseStatus, DialogService, ConfirmDialogComponent } from '@vality/ng-core';
import { combineLatest, defer, ReplaySubject } from 'rxjs';
import { filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
import {
DialogResponseStatus,
DialogService,
ConfirmDialogComponent,
Column,
createOperationColumn,
ComponentChanges,
} from '@vality/ng-core';
import { coerceBoolean } from 'coerce-property';
import { filter, switchMap } from 'rxjs/operators';
import { NotificationErrorService } from '@cc/app/shared/services/notification-error';
@ -36,47 +41,15 @@ export class RoutingRulesListComponent<
T extends { [N in PropertyKey]: unknown } & DelegateId = {
[N in PropertyKey]: unknown;
} & DelegateId,
> {
> implements OnChanges
{
@Input() data: T[];
@Input() displayedColumns: { key: keyof T; name: string }[];
@Input() set data(data: T[]) {
this.data$.next(data);
}
@Input() @coerceBoolean progress: boolean | '' = false;
@Output() toDetails = new EventEmitter<DelegateId>();
@ViewChild(MatPaginator) set paginator(paginator: MatPaginator) {
this.paginator$.next(paginator);
}
dataSource$ = combineLatest([
defer(() => this.data$),
defer(() => this.paginator$).pipe(startWith(null)),
]).pipe(
map(([d, paginator]) => {
const data = new MatTableDataSource(d);
data.paginator = paginator;
return data;
}),
shareReplay(1),
);
get allDisplayedColumns() {
if (!this.displayedColumns) {
return [];
}
return this.displayedColumns
.concat([
{
key: 'actions',
name: 'Actions',
},
])
.map(({ key }) => key);
}
private data$ = new ReplaySubject<T[]>(1);
private paginator$ = new ReplaySubject<MatPaginator>(1);
columns: Column<T>[] = [];
constructor(
private dialogService: DialogService,
@ -85,6 +58,61 @@ export class RoutingRulesListComponent<
private route: ActivatedRoute,
) {}
ngOnChanges(changes: ComponentChanges<RoutingRulesListComponent<T>>) {
if (changes.displayedColumns) {
this.columns = [
...this.displayedColumns.map(
(c, idx): Column<T> => ({
field: `${c.key as string}.text`,
formatter:
idx === 0
? (d) => {
const v = d?.[c.key] as { caption: string; text: string };
return v?.text || `#${v?.caption}`;
}
: undefined,
click:
idx === 0
? (d) =>
this.toDetails.emit({
parentRefId: d?.parentRefId,
delegateIdx: d?.delegateIdx,
})
: undefined,
header: c.name,
description: `${c.key as string}.caption`,
}),
),
createOperationColumn([
{
label: 'Details',
click: (d) =>
this.toDetails.emit({
parentRefId: d?.parentRefId,
delegateIdx: d?.delegateIdx,
}),
},
{
label: 'Change delegate ruleset',
click: (d) => this.changeDelegateRuleset(d),
},
{
label: 'Change main ruleset',
click: (d) => this.changeTarget(d),
},
{
label: 'Clone delegate ruleset',
click: (d) => this.cloneDelegateRuleset(d),
},
{
label: 'Delete',
click: (d) => this.delete(d),
},
]),
];
}
}
changeDelegateRuleset(delegateId: DelegateId) {
this.dialogService
.open(ChangeDelegateRulesetDialogComponent, {

View File

@ -6,7 +6,7 @@ import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatTableModule } from '@angular/material/table';
import { TableModule } from '@vality/ng-core';
import { RoutingRulesListComponent } from './routing-rules-list.component';
@ -15,12 +15,11 @@ import { RoutingRulesListComponent } from './routing-rules-list.component';
CommonModule,
MatMenuModule,
MatCardModule,
MatTableModule,
MatPaginatorModule,
MatTableModule,
MatIconModule,
FlexLayoutModule,
MatButtonModule,
TableModule,
],
declarations: [RoutingRulesListComponent],
exports: [RoutingRulesListComponent],

View File

@ -57,7 +57,7 @@ export class RoutingRulesetComponent {
field: 'candidate',
description: 'description',
sortable: true,
formatter: (d) => this.getCandidateIdx(d).pipe(map((idx) => `${idx + 1}`)),
formatter: (d) => this.getCandidateIdx(d).pipe(map((idx) => `#${idx + 1}`)),
click: (d) => {
this.getCandidateIdx(d)
.pipe(untilDestroyed(this))
@ -129,6 +129,16 @@ export class RoutingRulesetComponent {
});
},
},
{
label: 'Duplicate',
click: (d) => {
this.getCandidateIdx(d)
.pipe(untilDestroyed(this))
.subscribe((idx) => {
void this.duplicateShopRule(idx);
});
},
},
{
label: 'Remove',
click: (d) => {
@ -176,7 +186,7 @@ export class RoutingRulesetComponent {
next: (res) => {
if (res.status === DialogResponseStatus.Success) {
this.domainStoreService.forceReload();
this.log.successOperation('update', 'Routing rule');
this.log.successOperation('create', 'Routing rule');
}
},
error: (err) => {
@ -217,6 +227,38 @@ export class RoutingRulesetComponent {
});
}
duplicateShopRule(idx: number) {
this.routingRulesetService.refID$
.pipe(
first(),
switchMap((refId) => this.routingRulesService.getShopCandidate(refId, idx)),
withLatestFrom(this.routingRulesetService.refID$),
switchMap(([shopCandidate, refId]) =>
this.dialog
.open(DomainThriftFormDialogComponent<RoutingCandidate>, {
type: 'RoutingCandidate',
title: 'Add shop routing candidate',
object: shopCandidate,
actionType: 'create',
action: (params) => this.routingRulesService.addShopRule(refId, params),
})
.afterClosed(),
),
)
.pipe(untilDestroyed(this))
.subscribe({
next: (res) => {
if (res.status === DialogResponseStatus.Success) {
this.domainStoreService.forceReload();
this.log.successOperation('create', 'Routing rule');
}
},
error: (err) => {
this.log.error(err);
},
});
}
removeShopRule(idx: number) {
this.routingRulesetService.removeShopRule(idx);
}

View File

@ -1 +0,0 @@
export * from './parties-table.module';

View File

@ -1,36 +0,0 @@
<table *ngIf="displayedColumns" [dataSource]="parties" mat-table>
<ng-container matColumnDef="email">
<th *matHeaderCellDef mat-header-cell>Email</th>
<td *matCellDef="let party" mat-cell>
{{ party.email }}
</td>
</ng-container>
<ng-container matColumnDef="id">
<th *matHeaderCellDef mat-header-cell>ID</th>
<td *matCellDef="let party" mat-cell>
{{ party.id }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="action-cell" mat-header-cell></th>
<td *matCellDef="let party" class="action-cell" mat-cell>
<button [matMenuTriggerFor]="menu" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button
*ngFor="let action of partyActions"
mat-menu-item
(click)="menuItemSelected(action, party.id)"
>
{{ action | ccPartyActions }}
</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>

View File

@ -1,7 +0,0 @@
table {
width: 100%;
}
.action-cell {
width: 8px;
}

View File

@ -1,32 +0,0 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Party, PartyID } from '@vality/domain-proto/domain';
import { PartyActions } from './party-actions';
import { PartyMenuItemEvent } from './party-menu-item-event';
@Component({
selector: 'cc-parties-table',
templateUrl: 'parties-table.component.html',
styleUrls: ['parties-table.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PartiesTableComponent {
@Input()
parties: Party[];
@Output()
menuItemSelected$: EventEmitter<PartyMenuItemEvent> = new EventEmitter();
partyActions = Object.keys(PartyActions);
displayedColumns = ['email', 'id', 'actions'];
menuItemSelected(action: string, partyID: PartyID): void {
switch (action) {
case PartyActions.NavigateToParty:
this.menuItemSelected$.emit({ action, partyID });
break;
default:
console.error('Wrong party action type.');
}
}
}

View File

@ -1,16 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatTableModule } from '@angular/material/table';
import { PartiesTableComponent } from './parties-table.component';
import { PartyActionsPipe } from './party-actions.pipe';
@NgModule({
exports: [PartiesTableComponent],
declarations: [PartiesTableComponent, PartyActionsPipe],
imports: [MatTableModule, MatMenuModule, MatButtonModule, MatIconModule, CommonModule],
})
export class PartiesTableModule {}

View File

@ -1,16 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { PartyActions } from './party-actions';
const PARTY_ACTION_NAMES: { [N in PartyActions]: string } = {
[PartyActions.NavigateToParty]: 'Merchant details',
};
@Pipe({
name: 'ccPartyActions',
})
export class PartyActionsPipe implements PipeTransform {
transform(action: string): string {
return PARTY_ACTION_NAMES[action] || action;
}
}

View File

@ -1,3 +0,0 @@
export enum PartyActions {
NavigateToParty = 'NavigateToParty',
}

View File

@ -1,8 +0,0 @@
import { PartyID } from '@vality/domain-proto/domain';
import { PartyActions } from './party-actions';
export interface PartyMenuItemEvent {
action: PartyActions;
partyID: PartyID;
}

View File

@ -6,18 +6,12 @@
(searchParamsChanged$)="searchParamsUpdated($event)"
></cc-parties-search-filters>
</mat-card-content>
<mat-card-footer *ngIf="inProgress$ | async">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card-footer>
</mat-card>
<ng-container *ngIf="parties$ | async as parties">
<cc-empty-search-result *ngIf="parties.length === 0"></cc-empty-search-result>
<mat-card *ngIf="parties.length > 0" fxLayout="column" fxLayoutGap="16px">
<cc-parties-table
[parties]="parties"
(menuItemSelected$)="partyMenuItemSelected($event)"
></cc-parties-table>
</mat-card>
</ng-container>
<v-table
[columns]="columns"
[data]="parties$ | async"
[progress]="inProgress$ | async"
noActions
></v-table>
</cc-page-layout>

View File

@ -1,11 +1,14 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Party } from '@vality/deanonimus-proto/deanonimus';
import { Column, createOperationColumn } from '@vality/ng-core';
import startCase from 'lodash-es/startCase';
import { FetchPartiesService } from '@cc/app/shared/services/fetch-parties.service';
import { getUnionKey } from '../../../utils';
import { PartiesSearchFiltersParams } from './parties-search-filters';
import { PartyActions } from './parties-table/party-actions';
import { PartyMenuItemEvent } from './parties-table/party-menu-item-event';
import { SearchPartiesService } from './search-parties.service';
@Component({
@ -17,6 +20,50 @@ export class SearchPartiesComponent {
initSearchParams$ = this.partiesService.data$;
inProgress$ = this.fetchPartiesService.inProgress$;
parties$ = this.fetchPartiesService.parties$;
columns: Column<Party>[] = [
{
field: 'email',
description: 'id',
pinned: 'left',
link: (party) => `/party/${party.id}`,
},
{
field: 'blocking',
type: 'tag',
formatter: (party) => getUnionKey(party.blocking),
typeParameters: {
label: (party) => startCase(getUnionKey(party.blocking)),
tags: {
blocked: { color: 'warn' },
unblocked: { color: 'success' },
},
},
},
{
field: 'suspension',
type: 'tag',
formatter: (party) => getUnionKey(party.suspension),
typeParameters: {
label: (party) => startCase(getUnionKey(party.suspension)),
tags: {
suspended: { color: 'warn' },
active: { color: 'success' },
},
},
},
{
field: 'shops',
formatter: (party) => party.shops.size,
},
createOperationColumn([
{
label: 'Details',
click: (party) => {
this.router.navigate([`/party/${party.id}`]);
},
},
]),
];
constructor(
private partiesService: SearchPartiesService,
@ -28,11 +75,4 @@ export class SearchPartiesComponent {
this.partiesService.preserve($event);
this.fetchPartiesService.searchParties($event.text);
}
partyMenuItemSelected(event: PartyMenuItemEvent) {
switch (event.action) {
case PartyActions.NavigateToParty:
void this.router.navigate([`/party/${event.partyID}`]);
}
}
}

View File

@ -3,13 +3,13 @@ import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatCardModule } from '@angular/material/card';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { TableModule } from '@vality/ng-core';
import { EmptySearchResultModule } from '@cc/components/empty-search-result';
import { PageLayoutModule } from '../../shared';
import { PartiesSearchFiltersModule } from './parties-search-filters';
import { PartiesTableModule } from './parties-table';
import { SearchPartiesRoutingModule } from './search-parties-routing.module';
import { SearchPartiesComponent } from './search-parties.component';
@ -19,11 +19,11 @@ import { SearchPartiesComponent } from './search-parties.component';
FlexModule,
MatCardModule,
PartiesSearchFiltersModule,
PartiesTableModule,
CommonModule,
EmptySearchResultModule,
MatProgressBarModule,
PageLayoutModule,
TableModule,
],
declarations: [SearchPartiesComponent],
})

View File

@ -1 +0,0 @@
export * from './shop-details.module';

View File

@ -1,5 +0,0 @@
import { Services, RoutingConfig } from '@cc/app/shared/services';
export const ROUTING_CONFIG: RoutingConfig = {
services: [Services.PartyManagement],
};

View File

@ -1,61 +0,0 @@
import { Injectable } from '@angular/core';
import { Shop } from '@vality/domain-proto/domain';
import { PartyID, ShopID } from '@vality/domain-proto/payment_processing';
import { BehaviorSubject, Observable, defer, EMPTY, merge } from 'rxjs';
import { catchError, shareReplay, switchMap } from 'rxjs/operators';
import { PartyManagementService } from '@cc/app/api/payment-processing';
import { NotificationErrorService } from '@cc/app/shared/services/notification-error';
import { inProgressFrom, progressTo } from '@cc/utils';
@Injectable()
export class FetchShopService {
shop$: Observable<Shop> = defer(() => this.getShop$).pipe(
switchMap(({ partyID, shopID }) =>
this.partyManagementService.GetShop(partyID, shopID).pipe(
progressTo(this.progress$),
catchError((err) => {
this.notificationErrorService.error(
err,
'An error occurred while fetching shop',
);
return EMPTY;
}),
),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
contract$ = defer(() => this.getShop$).pipe(
switchMap(({ partyID, shopID }) =>
this.partyManagementService.GetShopContract(partyID, shopID).pipe(
progressTo(this.progress$),
catchError((err) => {
this.notificationErrorService.error(
err,
'An error occurred while fetching shop contract',
);
return EMPTY;
}),
),
),
shareReplay({ refCount: true, bufferSize: 1 }),
);
inProgress$ = inProgressFrom(() => this.progress$, merge(this.shop$, this.contract$));
private getShop$ = new BehaviorSubject<{ partyID: PartyID; shopID: ShopID }>(null);
private progress$ = new BehaviorSubject(0);
constructor(
private partyManagementService: PartyManagementService,
private notificationErrorService: NotificationErrorService,
) {}
getShop(partyID: PartyID, shopID: ShopID) {
this.getShop$.next({ shopID, partyID });
}
reload() {
if (this.getShop$.value) this.getShop$.next(this.getShop$.value);
}
}

View File

@ -1,21 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '@cc/app/shared/services';
import { ROUTING_CONFIG } from './routing-config';
import { ShopDetailsComponent } from './shop-details.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: ShopDetailsComponent,
canActivate: [AppAuthGuardService],
data: ROUTING_CONFIG,
},
]),
],
})
export class ShopDetailsRoutingModule {}

View File

@ -1,42 +0,0 @@
<div fxLayout="column" fxLayoutGap="24px">
<cc-headline>Shop details</cc-headline>
<mat-card>
<mat-card-content>
<cc-json-viewer
*ngIf="shop$ | async as shop"
[extensions]="extensions$ | async"
[metadata]="metadata$ | async"
[value]="shop"
namespace="domain"
type="Shop"
></cc-json-viewer>
<div *ngIf="inProgress$ | async" fxLayout fxLayoutAlign="center center">
<mat-spinner diameter="64"></mat-spinner>
</div>
</mat-card-content>
</mat-card>
<mat-card>
<mat-card-title>Contract</mat-card-title>
<mat-card-content>
<cc-json-viewer
*ngIf="contract$ | async as contract"
[extensions]="extensions$ | async"
[metadata]="metadata$ | async"
[value]="contract"
namespace="domain"
type="Contract"
></cc-json-viewer>
<div *ngIf="inProgress$ | async" fxLayout fxLayoutAlign="center center">
<mat-spinner diameter="64"></mat-spinner>
</div>
</mat-card-content>
</mat-card>
<v-actions *ngIf="shop$ | async as shop">
<button mat-raised-button (click)="toggleSuspension()">
{{ (shop.suspension | ccUnionKey) === 'active' ? 'Suspend' : 'Activate' }}
</button>
<button color="warn" mat-raised-button (click)="toggleBlocking()">
{{ (shop.blocking | ccUnionKey) === 'unblocked' ? 'Block' : 'Unblock' }}
</button>
</v-actions>
</div>

View File

@ -1,112 +0,0 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntilDestroy } from '@ngneat/until-destroy';
import { DialogService, DialogResponseStatus, ConfirmDialogComponent } from '@vality/ng-core';
import { combineLatest, switchMap, from } from 'rxjs';
import { pluck, filter, withLatestFrom, first, map } from 'rxjs/operators';
import { DomainMetadataViewExtensionsService } from '@cc/app/shared/components/thrift-api-crud/domain/domain-thrift-viewer/services/domain-metadata-view-extensions';
import { NotificationErrorService } from '@cc/app/shared/services/notification-error';
import { getUnionKey } from '../../../utils';
import { PartyManagementService } from '../../api/payment-processing';
import { NotificationService } from '../../shared/services/notification';
import { FetchShopService } from './services/fetch-shop.service';
@UntilDestroy()
@Component({
templateUrl: 'shop-details.component.html',
providers: [FetchShopService],
})
export class ShopDetailsComponent {
partyID$ = this.route.params.pipe(pluck('partyID'));
shopID$ = this.route.params.pipe(pluck('shopID'));
shop$ = this.fetchShopService.shop$;
contract$ = this.fetchShopService.contract$.pipe(map((c) => c?.contract));
inProgress$ = this.fetchShopService.inProgress$;
metadata$ = from(import('@vality/domain-proto/metadata.json').then((m) => m.default));
extensions$ = this.domainMetadataViewExtensionsService.extensions$;
constructor(
private fetchShopService: FetchShopService,
private route: ActivatedRoute,
private partyManagementService: PartyManagementService,
private dialogService: DialogService,
private notificationErrorService: NotificationErrorService,
private notificationService: NotificationService,
private domainMetadataViewExtensionsService: DomainMetadataViewExtensionsService,
) {
combineLatest([this.partyID$, this.shopID$]).subscribe(([partyID, shopID]) => {
this.fetchShopService.getShop(partyID, shopID);
});
}
toggleBlocking() {
this.shop$
.pipe(
first(),
switchMap((shop) =>
this.dialogService
.open(ConfirmDialogComponent, {
title:
getUnionKey(shop.blocking) === 'unblocked'
? 'Block shop'
: 'Unblock shop',
hasReason: true,
})
.afterClosed(),
),
filter((r) => r.status === DialogResponseStatus.Success),
withLatestFrom(this.shop$, this.partyID$),
switchMap(([{ data }, shop, partyID]) =>
getUnionKey(shop.blocking) === 'unblocked'
? this.partyManagementService.BlockShop(partyID, shop.id, data.reason)
: this.partyManagementService.UnblockShop(partyID, shop.id, data.reason),
),
)
.subscribe({
next: () => {
this.fetchShopService.reload();
this.notificationService.success();
},
error: (err) => {
this.notificationErrorService.error(err);
},
});
}
toggleSuspension() {
this.shop$
.pipe(
first(),
switchMap((shop) =>
this.dialogService
.open(ConfirmDialogComponent, {
title:
getUnionKey(shop.suspension) === 'active'
? 'Suspend shop'
: 'Activate shop',
})
.afterClosed(),
),
filter((r) => r.status === DialogResponseStatus.Success),
withLatestFrom(this.shop$, this.partyID$),
switchMap(([, shop, partyID]) =>
getUnionKey(shop.suspension) === 'active'
? this.partyManagementService.SuspendShop(partyID, shop.id)
: this.partyManagementService.ActivateShop(partyID, shop.id),
),
)
.subscribe({
next: () => {
this.fetchShopService.reload();
this.notificationService.success();
},
error: (err) => {
this.notificationErrorService.error(err);
},
});
}
}

View File

@ -1,32 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ActionsModule } from '@vality/ng-core';
import { JsonViewerModule } from '@cc/app/shared/components/json-viewer';
import { HeadlineModule } from '@cc/components/headline';
import { ThriftPipesModule } from '../../shared';
import { ShopDetailsRoutingModule } from './shop-details-routing.module';
import { ShopDetailsComponent } from './shop-details.component';
@NgModule({
imports: [
ShopDetailsRoutingModule,
HeadlineModule,
FlexModule,
MatCardModule,
CommonModule,
MatProgressSpinnerModule,
ActionsModule,
MatButtonModule,
ThriftPipesModule,
JsonViewerModule,
],
declarations: [ShopDetailsComponent],
})
export class ShopDetailsModule {}

View File

@ -1,4 +1,4 @@
<v-dialog [title]="dialogData.title">
<v-dialog [title]="dialogData.title" fullSize>
<cc-domain-thrift-form
[formControl]="control"
[namespace]="dialogData.namespace"
@ -7,7 +7,7 @@
<v-dialog-actions>
<button [disabled]="control.invalid" color="primary" mat-button (click)="upsert()">
{{ isUpdate ? 'Update' : 'Add' }}
{{ actionType === 'update' ? 'Update' : 'Add' }}
</button>
</v-dialog-actions>
</v-dialog>

View File

@ -2,7 +2,7 @@ import { Component, Injector } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { DialogSuperclass, DialogModule } from '@vality/ng-core';
import { DialogSuperclass, DialogModule, DEFAULT_DIALOG_CONFIG } from '@vality/ng-core';
import { ValueType } from '@vality/thrift-ts';
import { Observable } from 'rxjs';
@ -22,13 +22,19 @@ export class DomainThriftFormDialogComponent<T = unknown, R = unknown> extends D
action: (object: T) => Observable<R>;
namespace?: string;
object?: T;
actionType?: 'create' | 'update';
},
{ object?: T; result?: R; error?: unknown }
> {
static defaultDialogConfig = DEFAULT_DIALOG_CONFIG.large;
control = this.fb.control(this.dialogData.object ?? null);
get isUpdate() {
return this.dialogData.object !== undefined;
get actionType() {
return (
this.dialogData.actionType ??
(this.dialogData.object !== undefined ? 'update' : 'create')
);
}
constructor(

View File

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

View File

@ -6,6 +6,7 @@ import { createControlProviders, getImportValue } from '@vality/ng-core';
import { DomainMetadataFormExtensionsService } from '../../../../services';
import { MetadataFormModule } from '../../../metadata-form';
import { ThriftEditorModule } from '../../../thrift-editor';
import { BaseThriftFormSuperclass } from '../../thrift-forms/utils/thrift-form-superclass';
@Component({
@ -13,7 +14,7 @@ import { BaseThriftFormSuperclass } from '../../thrift-forms/utils/thrift-form-s
selector: 'cc-domain-thrift-form',
templateUrl: './domain-thrift-form.component.html',
providers: createControlProviders(() => DomainThriftFormComponent),
imports: [CommonModule, ReactiveFormsModule, MetadataFormModule],
imports: [CommonModule, ReactiveFormsModule, MetadataFormModule, ThriftEditorModule],
})
export class DomainThriftFormComponent extends BaseThriftFormSuperclass {
metadata$ = getImportValue<ThriftAstMetadata[]>(import('@vality/domain-proto/metadata.json'));

View File

@ -1,6 +1,7 @@
<cc-thrift-viewer
[extensions]="extensions$ | async"
[metadata]="metadata$ | async"
[progress]="progress"
[type]="type"
[value]="value"
namespace="domain"

View File

@ -3,6 +3,7 @@ import { Component, Input } from '@angular/core';
import { ThriftAstMetadata } from '@vality/domain-proto';
import { getImportValue } from '@vality/ng-core';
import { ValueType } from '@vality/thrift-ts';
import { coerceBoolean } from 'coerce-property';
import { ThriftViewerModule } from '../../../thrift-viewer';
@ -17,6 +18,7 @@ import { DomainMetadataViewExtensionsService } from './services/domain-metadata-
export class DomainThriftViewerComponent<T> {
@Input() value: T;
@Input() type: ValueType;
@Input() @coerceBoolean progress: boolean | '' = false;
// @Input() extensions?: MetadataViewExtension[];
metadata$ = getImportValue<ThriftAstMetadata[]>(import('@vality/domain-proto/metadata.json'));

View File

@ -24,7 +24,7 @@ export enum EditorKind {
providers: createControlProviders(() => ThriftEditorComponent),
})
export class ThriftEditorComponent<T> extends ValidatedFormControlSuperclass<T> {
@Input() kind: EditorKind = EditorKind.Editor;
@Input() kind: EditorKind = EditorKind.Form;
@Input() defaultValue?: T;

View File

@ -1,32 +1,35 @@
<div class="wrapper" gdColumns="1fr" gdGap="8px" gdRows="1fr auto">
<cc-monaco-diff-editor
*ngIf="isDiff; else standard"
[modified]="comparedFile$ | async"
[options]="{ renderSideBySide: true, readOnly: true }"
[original]="valueFile$ | async"
class="monaco-editor"
></cc-monaco-diff-editor>
<ng-template #standard>
<cc-json-viewer
*ngIf="kind === 'component'"
[extensions]="extensions"
[metadata]="metadata"
[namespace]="namespace"
[type]="type"
[value]="value"
></cc-json-viewer>
<cc-monaco-editor
*ngIf="kind === 'editor'"
[file]="valueFile$ | async"
[options]="{ readOnly: true }"
<div *ngIf="progress; else loaded" fxLayoutAlign="center"><mat-spinner></mat-spinner></div>
<ng-template #loaded>
<div class="wrapper" gdColumns="1fr" gdGap="8px" gdRows="1fr auto">
<cc-monaco-diff-editor
*ngIf="isDiff; else standard"
[modified]="comparedFile$ | async"
[options]="{ renderSideBySide: true, readOnly: true }"
[original]="valueFile$ | async"
class="monaco-editor"
>
</cc-monaco-editor>
<div class="actions" fxLayout fxLayoutAlign="end center" fxLayoutGap="2px">
<button color="primary" mat-icon-button (click)="toggleKind()">
<mat-icon *ngIf="kind === 'component'">code_blocks</mat-icon>
<mat-icon *ngIf="kind === 'editor'">view_comfy_alt</mat-icon>
</button>
</div>
</ng-template>
</div>
></cc-monaco-diff-editor>
<ng-template #standard>
<cc-json-viewer
*ngIf="kind === 'component'"
[extensions]="extensions"
[metadata]="metadata"
[namespace]="namespace"
[type]="type"
[value]="value"
></cc-json-viewer>
<cc-monaco-editor
*ngIf="kind === 'editor'"
[file]="valueFile$ | async"
[options]="{ readOnly: true }"
class="monaco-editor"
>
</cc-monaco-editor>
<div class="actions" fxLayout fxLayoutAlign="end center" fxLayoutGap="2px">
<button color="primary" mat-icon-button (click)="toggleKind()">
<mat-icon *ngIf="kind === 'component'">code_blocks</mat-icon>
<mat-icon *ngIf="kind === 'editor'">view_comfy_alt</mat-icon>
</button>
</div>
</ng-template>
</div>
</ng-template>

View File

@ -3,6 +3,7 @@ import { UntilDestroy } from '@ngneat/until-destroy';
import { ThriftAstMetadata } from '@vality/domain-proto';
import { ComponentChanges } from '@vality/ng-core';
import { ValueType } from '@vality/thrift-ts';
import { coerceBoolean } from 'coerce-property';
import { ReplaySubject } from 'rxjs';
import { MetadataViewExtension } from '@cc/app/shared/components/json-viewer';
@ -26,6 +27,7 @@ export class ThriftViewerComponent<T> implements OnChanges {
@Input() kind: ViewerKind = ViewerKind.Component;
@Input() value: T;
@Input() compared?: T;
@Input() @coerceBoolean progress: boolean | '' = false;
@Input() metadata: ThriftAstMetadata[];
@Input() namespace: string;

View File

@ -5,6 +5,7 @@ import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MetadataFormModule } from '@cc/app/shared/components/metadata-form';
import { MonacoEditorModule } from '@cc/components/monaco-editor';
@ -27,6 +28,7 @@ import { ThriftViewerComponent } from './thrift-viewer.component';
FlexModule,
JsonViewerModule,
GridModule,
MatProgressSpinnerModule,
],
})
export class ThriftViewerModule {}