TD-449: Support thrift viewer ext (#161)

This commit is contained in:
Rinat Arsaev 2022-11-16 20:23:13 +06:00 committed by GitHub
parent ae2fff4ba1
commit 7136470378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 749 additions and 471 deletions

48
package-lock.json generated
View File

@ -30,7 +30,7 @@
"@s-libs/ng-core": "14.0.0",
"@s-libs/rxjs-core": "14.0.0",
"@vality/deanonimus-proto": "1.0.1-c9a6cae.0",
"@vality/domain-proto": "1.0.1-09e7a75.0",
"@vality/domain-proto": "1.0.1-d59017c.0",
"@vality/dominant-cache-proto": "1.0.1-5b29d81.0",
"@vality/file-storage-proto": "1.0.1-447212b.0",
"@vality/fistful-proto": "1.0.1-9c78e89.0",
@ -61,6 +61,7 @@
"tslib": "2.3.1",
"utility-types": "3.10.0",
"uuid": "3.3.3",
"yaml": "2.1.3",
"zone.js": "0.11.4"
},
"devDependencies": {
@ -5606,9 +5607,9 @@
"integrity": "sha512-GjD4N6ZXyuYaGXv4od+lcCGKfIJFi640I6wJ3+O1eA9VrUI4Ro+Ljpu+TLjckMQ6nS1DkXuu+06Dk7Hr5nK1og=="
},
"node_modules/@vality/domain-proto": {
"version": "1.0.1-09e7a75.0",
"resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-1.0.1-09e7a75.0.tgz",
"integrity": "sha512-y7J2P98eehI7upxTPgg2vmxoBMV7QcgKstYiJBjJmt+Ra1XKhCeoGbRVOINmJqhHAbqGBpuqOAOLI1f7UZ9jLA=="
"version": "1.0.1-d59017c.0",
"resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-1.0.1-d59017c.0.tgz",
"integrity": "sha512-6hJYySG2O4guJ9NZdSzKtHLMBmef17fpfBxyCuVpnj+i56p3QEr7E+57UOAqwFaxV2Muw9lO7KkmVNWrtJ4eHQ=="
},
"node_modules/@vality/dominant-cache-proto": {
"version": "1.0.1-5b29d81.0",
@ -9559,6 +9560,15 @@
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"engines": {
"node": ">= 6"
}
},
"node_modules/create-ecdh": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
@ -22496,12 +22506,11 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true,
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
"engines": {
"node": ">= 6"
"node": ">= 14"
}
},
"node_modules/yargs": {
@ -26585,9 +26594,9 @@
"integrity": "sha512-GjD4N6ZXyuYaGXv4od+lcCGKfIJFi640I6wJ3+O1eA9VrUI4Ro+Ljpu+TLjckMQ6nS1DkXuu+06Dk7Hr5nK1og=="
},
"@vality/domain-proto": {
"version": "1.0.1-09e7a75.0",
"resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-1.0.1-09e7a75.0.tgz",
"integrity": "sha512-y7J2P98eehI7upxTPgg2vmxoBMV7QcgKstYiJBjJmt+Ra1XKhCeoGbRVOINmJqhHAbqGBpuqOAOLI1f7UZ9jLA=="
"version": "1.0.1-d59017c.0",
"resolved": "https://registry.npmjs.org/@vality/domain-proto/-/domain-proto-1.0.1-d59017c.0.tgz",
"integrity": "sha512-6hJYySG2O4guJ9NZdSzKtHLMBmef17fpfBxyCuVpnj+i56p3QEr7E+57UOAqwFaxV2Muw9lO7KkmVNWrtJ4eHQ=="
},
"@vality/dominant-cache-proto": {
"version": "1.0.1-5b29d81.0",
@ -29841,6 +29850,14 @@
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"dependencies": {
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true
}
}
},
"create-ecdh": {
@ -39557,10 +39574,9 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
},
"yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"dev": true
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
"integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg=="
},
"yargs": {
"version": "17.4.1",

View File

@ -44,7 +44,7 @@
"@s-libs/ng-core": "14.0.0",
"@s-libs/rxjs-core": "14.0.0",
"@vality/deanonimus-proto": "1.0.1-c9a6cae.0",
"@vality/domain-proto": "1.0.1-09e7a75.0",
"@vality/domain-proto": "1.0.1-d59017c.0",
"@vality/dominant-cache-proto": "1.0.1-5b29d81.0",
"@vality/file-storage-proto": "1.0.1-447212b.0",
"@vality/fistful-proto": "1.0.1-9c78e89.0",
@ -75,6 +75,7 @@
"tslib": "2.3.1",
"utility-types": "3.10.0",
"uuid": "3.3.3",
"yaml": "2.1.3",
"zone.js": "0.11.4"
},
"devDependencies": {

View File

@ -0,0 +1,8 @@
/**
* Use with sorting (arr.sort(fn))
*/
export function compareDifferentTypes<T>(a: T, b: T): number {
return typeof a === 'number' && typeof b === 'number'
? a - b
: String(a).localeCompare(String(b));
}

View File

@ -1,3 +1,5 @@
export * from './clean';
export * from './inline-json';
export * from './compare-different-types';
export * from './split-ids';
export * from './is-empty';

View File

@ -0,0 +1,7 @@
import _isEmpty from 'lodash-es/isEmpty';
import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject';
export function isEmpty(value: unknown): boolean {
return isObject(value) ? _isEmpty(value) : isNil(value) || value === '';
}

View File

@ -27,8 +27,8 @@ export function isPrimitiveType(type: ValueType): type is ThriftType {
return PRIMITIVE_TYPES.includes(type as never);
}
export const STRUCTURE_TYPES = ['typedef', 'struct', 'union', 'exception', 'enum'] as const;
export type StructureType = typeof STRUCTURE_TYPES[number];
export type StructureType = keyof JsonAST;
export const STRUCTURE_TYPES: StructureType[] = ['typedef', 'struct', 'union', 'exception', 'enum'];
export interface NamespaceObjectType {
namespaceMetadata: ThriftAstMetadata;
@ -48,9 +48,9 @@ export function parseNamespaceObjectType(
namespaceMetadata = metadata.reverse().find((m) => m.path === include[namespace].path);
if (!namespaceMetadata)
namespaceMetadata = metadata.reverse().find((m) => m.name === namespace);
const objectType = (Object.keys(namespaceMetadata.ast) as StructureType[]).find(
const objectType = Object.keys(namespaceMetadata.ast).find(
(t) => namespaceMetadata.ast[t][type]
);
) as StructureType;
if (!objectType || !STRUCTURE_TYPES.includes(objectType)) {
throw new Error(`Unknown thrift structure type: ${objectType}`);
}

View File

@ -9,9 +9,13 @@
>
<div class="details-container" fxLayout="column" fxLayoutGap="24px">
<cc-thrift-viewer
[extensions]="extensions$ | async"
[kind]="kind"
[value]="objWithRef?.obj | ccUnionValue"
[metadata]="metadata$ | async"
[value]="objWithRef?.obj"
class="viewer"
namespace="domain"
type="DomainObject"
(changeKind)="kind = $event"
></cc-thrift-viewer>
<cc-actions>

View File

@ -4,8 +4,11 @@ import { Router } from '@angular/router';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { DomainObject, Reference } from '@vality/domain-proto/lib/domain';
import { BaseDialogService, BaseDialogResponseStatus } from '@vality/ng-core';
import { from } from 'rxjs';
import { filter, switchMap } from 'rxjs/operators';
import { DomainMetadataViewExtensionsService } from '@cc/app/shared/services/domain-metadata-view-extensions';
import { ConfirmActionDialogComponent } from '../../../components/confirm-action-dialog';
import { enumHasValue } from '../../../utils';
import { ViewerKind } from '../../shared/components/thrift-viewer';
@ -26,6 +29,8 @@ export class DomainInfoComponent {
version$ = this.domainStoreService.version$;
progress$ = this.domainStoreService.isLoading$;
objWithRef: { obj: DomainObject; ref: Reference } = null;
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions$ = this.domainMetadataViewExtensionsService.extensions$;
get kind() {
const kind = localStorage.getItem(VIEWER_KIND);
@ -44,7 +49,8 @@ export class DomainInfoComponent {
private domainStoreService: DomainStoreService,
private baseDialogService: BaseDialogService,
private notificationService: NotificationService,
private errorService: ErrorService
private errorService: ErrorService,
private domainMetadataViewExtensionsService: DomainMetadataViewExtensionsService
) {}
edit() {

View File

@ -16,9 +16,13 @@
></cc-thrift-editor>
<cc-thrift-viewer
*ngIf="review"
[extensions]="viewerExtensions$ | async"
[kind]="reviewKind"
[metadata]="metadata$ | async"
[value]="control.value"
class="editor"
namespace="domain"
type="DomainObject"
(changeKind)="reviewKind = $event"
></cc-thrift-viewer>
</mat-card-content>

View File

@ -5,6 +5,8 @@ import { DomainObject } from '@vality/domain-proto/lib/domain';
import { BehaviorSubject } from 'rxjs';
import { withLatestFrom } from 'rxjs/operators';
import { DomainMetadataViewExtensionsService } from '@cc/app/shared/services/domain-metadata-view-extensions';
import { progressTo, getUnionKey, enumHasValue } from '../../../utils';
import { EditorKind } from '../../shared/components/thrift-editor';
import { ViewerKind } from '../../shared/components/thrift-viewer';
@ -29,6 +31,7 @@ export class DomainObjCreationComponent {
metadata$ = this.metadataService.metadata;
extensions$ = this.domainMetadataFormExtensionsService.extensions$;
viewerExtensions$ = this.domainMetadataViewExtensionsService.extensions$;
progress$ = new BehaviorSubject(0);
get kind() {
@ -57,6 +60,7 @@ export class DomainObjCreationComponent {
constructor(
private domainMetadataFormExtensionsService: DomainMetadataFormExtensionsService,
private domainMetadataViewExtensionsService: DomainMetadataViewExtensionsService,
private domainStoreService: DomainStoreService,
private notificationService: NotificationService,
private errorService: ErrorService,

View File

@ -36,7 +36,13 @@
<mat-expansion-panel-header>
{{ name | keyTitle | titlecase }}
</mat-expansion-panel-header>
<cc-json-viewer [json]="modification" [patches]="patches"></cc-json-viewer>
<cc-json-viewer
[extensions]="extensions$ | async"
[metadata]="metadata$ | async"
[value]="modificationUnit?.modification"
namespace="claim_management"
type="Modification"
></cc-json-viewer>
</mat-expansion-panel>
</ng-template>
</cc-timeline-item-content>

View File

@ -4,13 +4,13 @@ import { Claim, ModificationUnit } from '@vality/domain-proto/lib/claim_manageme
import { BaseDialogResponseStatus, BaseDialogService } from '@vality/ng-core';
import { coerceBoolean } from 'coerce-property';
import isEmpty from 'lodash-es/isEmpty';
import { BehaviorSubject, switchMap } from 'rxjs';
import { BehaviorSubject, switchMap, from } from 'rxjs';
import { filter, first } from 'rxjs/operators';
import { ClaimManagementService } from '@cc/app/api/claim-management';
import { PartyManagementService } from '@cc/app/api/payment-processing';
import { getModificationName } from '@cc/app/sections/claim/utils/get-modification-name';
import { Patch } from '@cc/app/shared/components/json-viewer';
import { DomainMetadataViewExtensionsService } from '@cc/app/shared/services/domain-metadata-view-extensions';
import { NotificationService } from '@cc/app/shared/services/notification';
import { Color, StatusColor } from '@cc/app/styles';
import { ConfirmActionDialogComponent } from '@cc/components/confirm-action-dialog';
@ -33,11 +33,12 @@ export class ModificationUnitTimelineItemComponent {
@Input() title?: string;
@Input() icon?: string;
@Input() color?: StatusColor | Color;
@Input() patches?: Patch[];
@Output() claimChanged = new EventEmitter<void>();
isLoading$ = inProgressFrom(() => this.progress$);
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions$ = this.domainMetadataViewExtensionsService.extensions$;
private progress$ = new BehaviorSubject(0);
@ -45,19 +46,16 @@ export class ModificationUnitTimelineItemComponent {
private partyManagementService: PartyManagementService,
private baseDialogService: BaseDialogService,
private claimManagementService: ClaimManagementService,
private notificationService: NotificationService
private notificationService: NotificationService,
private domainMetadataViewExtensionsService: DomainMetadataViewExtensionsService
) {}
get name() {
return getModificationName(this.modificationUnit.modification);
}
get modification() {
return getUnionValue(getUnionValue(this.modificationUnit?.modification));
}
get hasModificationContent() {
return !isEmpty(this.modification);
return !isEmpty(getUnionValue(getUnionValue(this.modificationUnit?.modification)));
}
update() {

View File

@ -1,7 +1,6 @@
<cc-modification-unit-timeline-item
[claim]="claim"
[modificationUnit]="modificationUnit"
[patches]="extended$ | async"
isChangeable
(claimChanged)="claimChanged.emit()"
></cc-modification-unit-timeline-item>

View File

@ -1,13 +1,8 @@
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { Claim, ModificationUnit } from '@vality/domain-proto/lib/claim_management';
import { Category } from '@vality/dominant-cache-proto';
import { combineLatest, defer, of, ReplaySubject } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ReplaySubject } from 'rxjs';
import { DominantCacheService } from '@cc/app/api/dominant-cache';
import { ComponentChanges } from '@cc/app/shared';
import { NotificationService } from '@cc/app/shared/services/notification';
import { getUnionKey } from '@cc/utils';
@Component({
selector: 'cc-shop-modification-timeline-item',
@ -18,39 +13,8 @@ export class ShopModificationTimelineItemComponent implements OnChanges {
@Input() claim: Claim;
@Output() claimChanged = new EventEmitter<void>();
extended$ = combineLatest([
defer(() => this.modificationUnit$),
this.dominantCacheService.GetCategories().pipe(
catchError((err) => {
this.notificationService.error('Categories were not loaded');
console.error(err);
return of([] as Category[]);
})
),
]).pipe(
map(([modificationUnit, categories]) => {
const modification =
modificationUnit.modification.party_modification.shop_modification.modification;
switch (getUnionKey(modification)) {
case 'creation': {
const category = categories.find(
(c) => c.ref === String(modification.creation?.category?.id)
);
return [{ path: ['category', 'id'], value: category?.name }];
}
default:
return [];
}
})
);
private modificationUnit$ = new ReplaySubject<ModificationUnit>(1);
constructor(
private dominantCacheService: DominantCacheService,
private notificationService: NotificationService
) {}
ngOnChanges({ modificationUnit }: ComponentChanges<ShopModificationTimelineItemComponent>) {
if (modificationUnit) {
this.modificationUnit$.next(modificationUnit.currentValue);

View File

@ -2,7 +2,14 @@
<cc-headline>Shop details</cc-headline>
<mat-card>
<mat-card-content>
<cc-shop-main-info *ngIf="shop$ | async as shop" [shop]="shop"></cc-shop-main-info>
<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>
@ -13,7 +20,11 @@
<mat-card-content>
<cc-json-viewer
*ngIf="contract$ | async as contract"
[json]="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>

View File

@ -1,10 +1,12 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntilDestroy } from '@ngneat/until-destroy';
import { BaseDialogService, BaseDialogResponseStatus } from '@vality/ng-core';
import { combineLatest, switchMap } from 'rxjs';
import { combineLatest, switchMap, from } from 'rxjs';
import { pluck, filter, withLatestFrom, first, map } from 'rxjs/operators';
import { DomainMetadataViewExtensionsService } from '@cc/app/shared/services/domain-metadata-view-extensions';
import { ConfirmActionDialogComponent } from '../../../components/confirm-action-dialog';
import { getUnionKey } from '../../../utils';
import { PartyManagementService } from '../../api/payment-processing';
@ -16,7 +18,6 @@ import { FetchShopService } from './services/fetch-shop.service';
@Component({
templateUrl: 'shop-details.component.html',
providers: [FetchShopService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopDetailsComponent {
partyID$ = this.route.params.pipe(pluck('partyID'));
@ -25,6 +26,8 @@ export class ShopDetailsComponent {
shop$ = this.fetchShopService.shop$;
contract$ = this.fetchShopService.contract$.pipe(map((c) => c?.contract));
inProgress$ = this.fetchShopService.inProgress$;
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
extensions$ = this.domainMetadataViewExtensionsService.extensions$;
constructor(
private fetchShopService: FetchShopService,
@ -32,7 +35,8 @@ export class ShopDetailsComponent {
private partyManagementService: PartyManagementService,
private baseDialogService: BaseDialogService,
private errorService: ErrorService,
private notificationService: NotificationService
private notificationService: NotificationService,
private domainMetadataViewExtensionsService: DomainMetadataViewExtensionsService
) {
combineLatest([this.partyID$, this.shopID$]).subscribe(([partyID, shopID]) => {
this.fetchShopService.getShop(partyID, shopID);

View File

@ -12,12 +12,10 @@ import { HeadlineModule } from '@cc/components/headline';
import { ThriftPipesModule } from '../../shared';
import { ShopDetailsRoutingModule } from './shop-details-routing.module';
import { ShopDetailsComponent } from './shop-details.component';
import { ShopMainInfoModule } from './shop-main-info';
@NgModule({
imports: [
ShopDetailsRoutingModule,
ShopMainInfoModule,
HeadlineModule,
FlexModule,
MatCardModule,

View File

@ -1,4 +0,0 @@
<ng-container *ngIf="category$ | async as category; else isLoading">
{{ category?.name }} (ID: {{ categoryID }})
</ng-container>
<ng-template #isLoading> ID: {{ categoryID }} </ng-template>

View File

@ -1,29 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Category } from '@vality/domain-proto/lib/domain';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { DominantCacheService } from '@cc/app/api/dominant-cache';
@Component({
templateUrl: 'category.component.html',
selector: 'cc-category',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CategoryComponent {
@Input() set category(categoryID: number) {
this.categoryID = categoryID;
this.category$ = this.dominantCacheService
.GetCategories()
.pipe(
map((categories) =>
categories.find((category) => category.ref === String(categoryID))
)
);
}
category$: Observable<Category>;
categoryID: number;
constructor(private dominantCacheService: DominantCacheService) {}
}

View File

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

View File

@ -1,20 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Blocking } from '@vality/domain-proto/lib/domain';
import { getUnionKey } from '@cc/utils/get-union-key';
@Pipe({
name: 'ccBlockingPipe',
})
export class ShopBlockingPipe implements PipeTransform {
public transform(input: Blocking): string {
switch (getUnionKey(input)) {
case 'blocked':
return 'Blocked';
case 'unblocked':
return 'Unblocked';
default:
return '';
}
}
}

View File

@ -1,62 +0,0 @@
<div fxLayout="column" fxLayoutGap="16px">
<div
fxLayout="row"
fxLayout.lt-sm="column"
fxLayoutAlign="start center"
fxLayoutAlign.lt-sm="start"
fxLayoutGap="16px"
>
<cc-details-item fxFlex title="Name">{{ shop.details.name }}</cc-details-item>
<cc-details-item fxFlex title="Description">{{
shop.details.description ? shop.details.description : '-'
}}</cc-details-item>
<cc-details-item fxFlex title="Category">
<cc-category [category]="shop.category.id"></cc-category>
</cc-details-item>
</div>
<div
fxLayout="row"
fxLayout.lt-sm="column"
fxLayoutAlign="start center"
fxLayoutAlign.lt-sm="start"
fxLayoutGap="16px"
>
<cc-details-item fxFlex title="Created at">{{
shop.created_at | date: 'dd.MM.yyyy HH:mm:ss'
}}</cc-details-item>
<cc-details-item fxFlex title="URL"
><a href="{{ shop.location.url }}">{{ shop.location.url }}</a></cc-details-item
>
<cc-details-item fxFlex title="Currency">{{
shop.account?.currency?.symbolic_code ? shop.account.currency.symbolic_code : '-'
}}</cc-details-item>
</div>
<div
fxLayout="row"
fxLayout.lt-sm="column"
fxLayoutAlign="start center"
fxLayoutAlign.lt-sm="start"
fxLayoutGap="16px"
>
<cc-details-item fxFlex title="Blocking">{{
shop.blocking | ccBlockingPipe
}}</cc-details-item>
<cc-details-item fxFlex title="Suspension">
{{ shop.suspension | ccSuspensionPipe }}
</cc-details-item>
<div fxFlex></div>
</div>
<div
fxLayout="row"
fxLayout.lt-sm="column"
fxLayoutAlign="start center"
fxLayoutAlign.lt-sm="start"
fxLayoutGap="16px"
>
<cc-details-item fxFlex title="Payout tool ID">{{
shop.payout_tool_id ? shop.payout_tool_id : '-'
}}</cc-details-item>
<cc-details-item fxFlex title="Shop ID">{{ shop.id }}</cc-details-item>
<cc-details-item fxFlex title="Contract ID">{{ shop.contract_id }}</cc-details-item>
</div>
</div>

View File

@ -1,11 +0,0 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { Shop } from '@vality/domain-proto';
@Component({
selector: 'cc-shop-main-info',
templateUrl: 'shop-main-info.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopMainInfoComponent {
@Input() shop: Shop;
}

View File

@ -1,18 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { StatusModule } from '@cc/app/shared/components';
import { DetailsItemModule } from '@cc/components/details-item';
import { CategoryComponent } from './components/category/category.component';
import { ShopBlockingPipe } from './shop-blocking.pipe';
import { ShopMainInfoComponent } from './shop-main-info.component';
import { ShopSuspensionPipe } from './shop-suspension.pipe';
@NgModule({
imports: [FlexModule, DetailsItemModule, StatusModule, CommonModule],
declarations: [ShopMainInfoComponent, CategoryComponent, ShopBlockingPipe, ShopSuspensionPipe],
exports: [ShopMainInfoComponent],
})
export class ShopMainInfoModule {}

View File

@ -1,20 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { Suspension } from '@vality/domain-proto/lib/domain';
import { getUnionKey } from '@cc/utils/get-union-key';
@Pipe({
name: 'ccSuspensionPipe',
})
export class ShopSuspensionPipe implements PipeTransform {
public transform(input: Suspension): string {
switch (getUnionKey(input)) {
case 'active':
return 'Active';
case 'suspended':
return 'Suspended';
default:
return '';
}
}
}

View File

@ -1,3 +1,3 @@
<cc-base-dialog [title]="dialogData.title || 'Details'" noActions>
<cc-json-viewer [json]="dialogData.json"></cc-json-viewer>
<cc-json-viewer [value]="dialogData.json"></cc-json-viewer>
</cc-base-dialog>

View File

@ -0,0 +1,15 @@
<div>
<ng-container *ngIf="numberKey$ | async as numberKey; else defKey"
>{{ numberKey }}.</ng-container
>
<ng-template #defKey>
<ng-container *ngFor="let pathItem of keys; let idx = index">
<span [class]="(parentIsUnion(pathItem) | async) ? 'bold' : 'cc-secondary-text'"
>{{ (pathItem.key$ | async)?.renderValue$ | async | keyTitle | titlecase
}}{{
idx !== keys.length - 1 ? ((isUnion(pathItem) | async) ? ': ' : ' / ') : ''
}}</span
>
</ng-container>
</ng-template>
</div>

View File

@ -0,0 +1,3 @@
.bold {
font-weight: bold;
}

View File

@ -0,0 +1,43 @@
import { Component, Input, OnChanges } from '@angular/core';
import { of, switchMap, ReplaySubject } from 'rxjs';
import { map } from 'rxjs/operators';
import { ComponentChanges } from '@cc/app/shared';
import { MetadataViewItem } from '../../utils/metadata-view';
@Component({
selector: 'cc-key',
templateUrl: './key.component.html',
styleUrls: ['./key.component.scss'],
})
export class KeyComponent implements OnChanges {
@Input() keys?: MetadataViewItem[];
keys$ = new ReplaySubject<MetadataViewItem[]>(1);
numberKey$ = this.keys$.pipe(
switchMap((keys) => {
if (keys.length !== 1) return of(null);
return this.keys[0].key$.pipe(
switchMap((key) => key.renderValue$),
map((value) => {
if (typeof value === 'number') return `${value + 1}`;
return null;
})
);
})
);
ngOnChanges(changes: ComponentChanges<KeyComponent>) {
if (changes.keys) this.keys$.next(this.keys);
}
parentIsUnion(pathItem: MetadataViewItem) {
if (!pathItem?.data$) return of(false);
return pathItem.data$.pipe(map((data) => data?.trueParent?.objectType === 'union'));
}
isUnion(pathItem: MetadataViewItem) {
if (!pathItem?.data$) return of(false);
return pathItem.data$.pipe(map((data) => data?.trueTypeNode?.data?.objectType === 'union'));
}
}

View File

@ -1,2 +1,2 @@
export * from './json-viewer.module';
export * from './types/patch';
export * from './utils/metadata-view-extension';

View File

@ -1,30 +1,80 @@
<div gdColumns="1fr" gdGap="16px">
<div gdColumns="1fr 1fr 1fr" gdGap="16px">
<cc-details-item
*ngFor="let item of items; let i = index; trackBy: trackByFn"
[title]="item.key | keyTitle | titlecase"
<ng-container *ngIf="view?.items$ | async as items">
<div *ngIf="!(view.isValue$ | async); else onlyValue" gdColumns="1fr" gdGap="16px">
<div
*ngIf="(view.leaves$ | async)?.length as count"
[gdColumns]="count === 1 ? '1fr' : count === 2 ? '1fr 1fr' : '1fr 1fr 1fr'"
gdGap="16px"
>
<span
*ngIf="!!item.value"
[matTooltip]="item.tooltip"
fxLayoutAlign=" center"
fxLayoutGap="4px"
<div
*ngFor="let item of view.leaves$ | async; let i = index"
gdGap="8px"
gdRows="auto 1fr"
>
<span>{{ item.value }}</span>
<mat-icon *ngIf="item.tooltip" class="cc-secondary-text" inline>info</mat-icon>
</span>
</cc-details-item>
<cc-key [keys]="item.path$ | async" class="cc-caption cc-secondary-text"></cc-key>
<div *ngIf="item.current$ | async as current" class="cc-body-1">
<cc-json-viewer
*ngIf="current.value$ | async; else empty"
[data]="current.data$ | async"
[extension]="current.extension$ | async"
[extensions]="extensions"
[value]="current.value$ | async"
></cc-json-viewer>
<ng-template #empty>
<mat-icon class="cc-secondary-text" inline>hide_source</mat-icon>
</ng-template>
</div>
</div>
</div>
<ng-container *ngFor="let item of view?.nodes$ | async; let idx = index">
<mat-divider *ngIf="idx > 0 || (view.leaves$ | async)?.length"></mat-divider>
<div
[gdColumns]="((item.current$ | async)?.isNumberKey$ | async) ? 'auto 1fr' : '1fr'"
gdGap="16px"
>
<cc-key
*ngIf="!((item.current$ | async)?.key.data$ | async); else mapKey"
[class]="className"
[keys]="item.path$ | async"
></cc-key>
<ng-template #mapKey>
<cc-json-viewer
*ngIf="(item.current$ | async)?.key as key"
[data]="key.data$ | async"
[extensions]="extensions"
[level]="level + 1"
[value]="key.value$ | async"
></cc-json-viewer
></ng-template>
<cc-json-viewer
*ngIf="item.current$ | async as current"
[data]="current.data$ | async"
[extensions]="extensions"
[level]="level + 1"
[value]="current.value$ | async"
></cc-json-viewer>
</div>
</ng-container>
</div>
<!-- TODO: separate if 2 or more parents -->
<ng-container *ngFor="let item of objects; let i = index; trackBy: trackByFn">
<mat-divider *ngIf="i !== 0 || items.length"></mat-divider>
<span [class]="className">{{ item.key | keyTitle | titlecase }}</span>
<cc-json-viewer
*ngIf="!item.isEmpty"
[json]="item.sourceValue"
[patches]="patches"
[path]="item.path"
></cc-json-viewer>
</ng-container>
</div>
<ng-template #onlyValue>
<span class="cc-body-1">
<span
*ngIf="extension?.tooltip; else simpleValue"
[matTooltip]="getTooltip(extension.tooltip)"
matBadge=""
matBadgeOverlap="false"
matBadgeSize="small"
matTooltipClass="tooltip"
style="cursor: default"
>
{{ view.renderValue$ | async }}
</span>
<ng-template #simpleValue>
{{ view.renderValue$ | async }}
</ng-template>
</span>
</ng-template>
</ng-container>

View File

@ -1,50 +1,59 @@
import { Component, Input } from '@angular/core';
import isEqual from 'lodash-es/isEqual';
import isObject from 'lodash-es/isObject';
import { Component, Input, OnChanges } from '@angular/core';
import { ValueType, Field } from '@vality/thrift-ts';
import yaml from 'yaml';
import { InlineItem } from './types/inline-item';
import { Patch } from './types/patch';
import { getInline } from './utils/get-inline';
import { ThriftAstMetadata } from '@cc/app/api/utils';
import { MetadataFormData } from '../metadata-form';
import { MetadataViewItem } from './utils/metadata-view';
import {
MetadataViewExtension,
MetadataViewExtensionResult,
} from './utils/metadata-view-extension';
@Component({
selector: 'cc-json-viewer',
templateUrl: './json-viewer.component.html',
styleUrls: ['./json-viewer.scss'],
})
export class JsonViewerComponent {
@Input() json: unknown;
@Input() path: string[] = [];
export class JsonViewerComponent implements OnChanges {
@Input() value: unknown;
@Input() level = 0;
@Input() extension?: MetadataViewExtensionResult;
@Input() patches: Patch[] = [];
@Input() metadata: ThriftAstMetadata[];
@Input() namespace: string;
@Input() type: ValueType;
@Input() field?: Field;
@Input() parent?: MetadataFormData;
get inline(): InlineItem[] {
try {
return Object.entries(this.json)
.map(([k, v]) => getInline([k], v))
.filter(Boolean)
.map(
([path, value]): InlineItem =>
new InlineItem(
path,
value,
this.patches?.find((p) => isEqual(p.path, path))
)
)
.sort(({ key: a }, { key: b }) => a.localeCompare(b));
} catch (err) {
return [];
@Input() data: MetadataFormData;
@Input() extensions: MetadataViewExtension[];
view: MetadataViewItem;
className = this.getClassName();
ngOnChanges() {
if (this.metadata && this.namespace && this.type) {
try {
this.data = new MetadataFormData(
this.metadata,
this.namespace,
this.type,
this.field,
this.parent
);
} catch (err) {
this.data = undefined;
console.warn(err);
}
}
this.view = new MetadataViewItem(this.value, undefined, this.data, this.extensions);
this.className = this.getClassName();
}
get objects() {
return this.inline.filter(({ value }) => isObject(value));
}
get items() {
return this.inline.filter(({ value }) => !isObject(value));
}
get className() {
switch (this.path.length) {
getClassName() {
switch (this.level) {
case 0:
return 'cc-title';
case 1:
@ -56,7 +65,7 @@ export class JsonViewerComponent {
}
}
trackByFn(idx: number, item: InlineItem) {
return item.path.join(';');
getTooltip(tooltip: any) {
return yaml.stringify(tooltip);
}
}

View File

@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule, GridModule } from '@angular/flex-layout';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
@ -10,10 +11,11 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { ThriftPipesModule } from '@cc/app/shared';
import { DetailsItemModule } from '@cc/components/details-item';
import { KeyComponent } from './components/key/key.component';
import { JsonViewerComponent } from './json-viewer.component';
@NgModule({
declarations: [JsonViewerComponent],
declarations: [JsonViewerComponent, KeyComponent],
exports: [JsonViewerComponent],
imports: [
CommonModule,
@ -26,6 +28,7 @@ import { JsonViewerComponent } from './json-viewer.component';
MatButtonModule,
MatTooltipModule,
FlexModule,
MatBadgeModule,
],
})
export class JsonViewerModule {}

View File

@ -0,0 +1,5 @@
::ng-deep .tooltip {
display: block;
unicode-bidi: embed;
white-space: pre;
}

View File

@ -1,31 +0,0 @@
import isEmpty from 'lodash-es/isEmpty';
import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject';
import { Patch } from '../types/patch';
export class InlineItem {
get key() {
return this.patch?.key ?? this.path.join(' / ');
}
get value() {
return this.patch?.value ?? this.sourceValue;
}
get tooltip() {
return this.isPatched ? JSON.stringify(this.sourceValue, null, 2) : undefined;
}
get isPatched() {
return !!this.patch;
}
get isEmpty() {
return isObject(this.sourceValue) ? isEmpty(this.sourceValue) : isNil(this.sourceValue);
}
constructor(public path: string[], public sourceValue: unknown, private patch?: Patch) {
this.path = this.patch?.path ?? this.path;
}
}

View File

@ -1,6 +0,0 @@
export interface Patch {
path: string[];
key?: string;
value?: unknown;
tooltip?: string;
}

View File

@ -0,0 +1,33 @@
import { ValueType, Field, SetType, MapType, ListType } from '@vality/thrift-ts';
import { MetadataFormData, TypeGroup } from '../../metadata-form';
export function getChildrenTypes(sourceData: MetadataFormData): {
keyType?: ValueType;
valueType?: ValueType;
fields?: Field[];
} {
const data = sourceData.trueTypeNode.data;
switch (data.typeGroup) {
case TypeGroup.Object: {
switch (data.objectType) {
case 'struct':
return { fields: (data as MetadataFormData<ValueType, 'struct'>).ast };
case 'union':
return { fields: (data as MetadataFormData<ValueType, 'union'>).ast };
}
return;
}
case TypeGroup.Complex: {
if ((data as MetadataFormData<SetType | MapType | ListType>).type.name === 'map') {
return {
keyType: (data as MetadataFormData<MapType>).type.keyType,
valueType: (data as MetadataFormData<MapType>).type.valueType,
};
}
return {
valueType: (data as MetadataFormData<SetType | ListType>).type.valueType,
};
}
}
}

View File

@ -0,0 +1,8 @@
export function getEntries(obj: any): [number | string, any][] {
if (!obj) return [];
return Array.isArray(obj) || obj instanceof Set
? Array.from(obj).map((v, idx) => [idx, v])
: obj instanceof Map
? Array.from(obj)
: Object.entries(obj);
}

View File

@ -1,16 +0,0 @@
import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject';
export function getInline(path: string[], value: unknown): [string[], unknown] {
if (isNil(value)) {
return null;
}
if (isObject(value)) {
const entries: [string, unknown][] = Object.entries(value).filter(([, v]) => !isNil(v));
if (entries.length === 1) {
const [childKey, childValue] = entries[0];
return getInline([...path, childKey], childValue);
}
}
return [path, value];
}

View File

@ -0,0 +1,29 @@
import { Observable, combineLatest, switchMap, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { MetadataFormData } from '../../metadata-form';
export interface MetadataViewExtensionResult {
key?: string;
value: string;
tooltip?: any;
// link?: string;
}
export type MetadataViewExtension = {
determinant: (data: MetadataFormData, value: any) => Observable<boolean>;
extension: (data: MetadataFormData, value: any) => Observable<MetadataViewExtensionResult>;
};
export function getFirstDeterminedExtensionsResult(
sourceExtensions: MetadataViewExtension[],
data: MetadataFormData,
value: any
): Observable<MetadataViewExtensionResult> {
return sourceExtensions?.length
? combineLatest(sourceExtensions.map(({ determinant }) => determinant(data, value))).pipe(
map((determined) => sourceExtensions.find((_, idx) => determined[idx])),
switchMap((extension) => extension?.extension(data, value) ?? of(null))
)
: of(null);
}

View File

@ -0,0 +1,190 @@
import { isEmpty } from '@vality/ng-core';
import { SetType, ListType, MapType, ValueType } from '@vality/thrift-ts';
import isNil from 'lodash-es/isNil';
import isObject from 'lodash-es/isObject';
import { Observable, of, switchMap, combineLatest } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { MetadataFormData } from '../../metadata-form';
import { getChildrenTypes } from './get-children-types';
import { getEntries } from './get-entries';
import {
MetadataViewExtension,
getFirstDeterminedExtensionsResult,
} from './metadata-view-extension';
export class MetadataViewItem {
extension$ = getFirstDeterminedExtensionsResult(this.extensions, this.data, this.value).pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
data$ = this.extension$.pipe(map((ext) => (ext ? null : this.data)));
key$ = this.extension$.pipe(
map((ext) => (isNil(ext?.key) ? this.key : new MetadataViewItem(ext.key))),
shareReplay({ refCount: true, bufferSize: 1 })
);
value$ = this.extension$.pipe(
map((ext) => {
const value = ext?.value ?? this.value;
return isEmpty(value) ? null : value;
})
);
renderValue$ = combineLatest([this.value$, this.data$]).pipe(
map(([value, data]) => {
if (data?.trueTypeNode?.data?.objectType === 'enum')
return (
(data.trueTypeNode.data as MetadataFormData<ValueType, 'enum'>).ast.items.find(
(i) => i.value === value
).name ?? value
);
if (data?.objectType === 'union' && isEmpty(getEntries(value)?.[0]?.[1]))
return getEntries(value)?.[0]?.[0];
return value;
})
);
items$: Observable<MetadataViewItem[]> = this.createItems().pipe(
shareReplay({ refCount: true, bufferSize: 1 })
);
inline$: Observable<MetadataViewItem[]> = combineLatest([
this.items$,
this.key$,
this.data$,
this.key$.pipe(switchMap((key) => key?.value$ || of(null))),
]).pipe(
switchMap(([items, key, data, keyValue]) => {
if (
!items.length ||
items.length > 1 ||
isObject(keyValue) ||
key?.data ||
(data?.trueTypeNode?.data as MetadataFormData<SetType | ListType | MapType>)?.type
?.name
)
return of([]);
const [item] = items;
return combineLatest([
item.key$.pipe(switchMap((key) => key.value$)),
item.value$,
]).pipe(
switchMap(([childKey, childValue]) => {
if (
typeof childKey === 'number' ||
(data?.objectType === 'union' && isEmpty(childValue))
)
return of([]);
return item.data$.pipe(
switchMap((itemData) => {
if (data?.objectType === 'union' && itemData?.objectType !== 'union')
return of([item]);
return item.inline$.pipe(map((childInline) => [item, ...childInline]));
})
);
})
);
}),
shareReplay({ refCount: true, bufferSize: 1 })
);
path$: Observable<MetadataViewItem[]> = this.inline$.pipe(
map((inline) => {
return [this, ...inline];
}),
shareReplay({ refCount: true, bufferSize: 1 })
);
current$ = this.path$.pipe(map((keys) => keys.at(-1)));
isLeaf$ = combineLatest([
this.current$.pipe(switchMap((c) => c.items$)),
this.data$,
this.value$,
]).pipe(
map(([items, data, value]) => {
return (
!items.length ||
(data?.objectType === 'union' && isEmpty(getEntries(value)?.[0]?.[1]))
);
}),
shareReplay({ refCount: true, bufferSize: 1 })
);
isValue$ = combineLatest([
this.current$.pipe(switchMap((c) => c.items$)),
this.data$,
this.value$,
this.current$.pipe(map((c) => c.key)),
]).pipe(
map(([items, data, value, key]) => {
return (
(!items.length && !key) ||
(data?.objectType === 'union' && isEmpty(getEntries(value)?.[0]?.[1]))
);
})
);
leaves$ = this.items$.pipe(
switchMap((items) =>
combineLatest(
items.map((item) => item.isLeaf$.pipe(map((isLeaf) => (isLeaf ? item : null))))
)
),
map((items) => items.filter(Boolean)),
shareReplay({ refCount: true, bufferSize: 1 })
);
nodes$ = this.items$.pipe(
switchMap((items) =>
combineLatest(
items.map((item) => item.isLeaf$.pipe(map((isLeaf) => (isLeaf ? null : item))))
)
),
map((items) => items.filter(Boolean)),
shareReplay({ refCount: true, bufferSize: 1 })
);
isNumberKey$ = this.key$.pipe(map(({ value }) => typeof value === 'number'));
constructor(
private value: any,
private key?: MetadataViewItem,
private data?: MetadataFormData,
private extensions?: MetadataViewExtension[]
) {}
private createItems(): Observable<MetadataViewItem[]> {
return combineLatest([this.data$, this.value$]).pipe(
map(([data, value]) => {
if (data) {
const trueData = this.data.trueTypeNode.data;
if (
trueData.objectType === 'struct' ||
trueData.objectType === 'union' ||
(trueData as MetadataFormData<SetType | ListType | MapType>).type?.name
) {
const types = getChildrenTypes(trueData);
return getEntries(value).map(([itemKey, itemValue]) => {
return new MetadataViewItem(
itemValue,
types.keyType
? new MetadataViewItem(
itemKey,
undefined,
trueData.create({ type: types.keyType }),
this.extensions
)
: new MetadataViewItem(itemKey),
trueData.create({
field: types.fields?.find((f) => f.name === itemKey),
type: types.valueType,
}),
this.extensions
);
});
}
}
return isObject(value)
? getEntries(value).map(
([k, v]) => new MetadataViewItem(v, new MetadataViewItem(k))
)
: [];
})
);
}
}

View File

@ -48,7 +48,7 @@
<ng-container *ngIf="isKeyValue">
<span class="cc-body-2">Key</span>
<cc-metadata-form
[extensions]="data.extensions"
[extensions]="extensions"
[formControl]="keyControls.controls[i]"
[metadata]="data.metadata"
[namespace]="data.namespace"
@ -58,7 +58,7 @@
<span class="cc-body-2">Value</span>
</ng-container>
<cc-metadata-form
[extensions]="data.extensions"
[extensions]="extensions"
[formControl]="valueControl"
[metadata]="data.metadata"
[namespace]="data.namespace"

View File

@ -5,6 +5,7 @@ import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { FormComponentSuperclass } from '@s-libs/ng-core';
import { MapType, SetType, ListType } from '@vality/thrift-ts';
import { MetadataFormExtension } from '@cc/app/shared/components/metadata-form';
import { createControlProviders, getErrorsTree } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data';
@ -29,6 +30,7 @@ export class ComplexFormComponent<T extends unknown[] | Map<unknown, unknown> |
implements OnInit, Validator
{
@Input() data: MetadataFormData<SetType | MapType | ListType>;
@Input() extensions: MetadataFormExtension[];
valueControls = new FormArray([]);
keyControls = new FormArray([]);

View File

@ -1,5 +1,4 @@
import { Component, Input } from '@angular/core';
import { Enums } from '@vality/thrift-ts/src/thrift-parser';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
@ -11,5 +10,5 @@ import { MetadataFormData } from '../../types/metadata-form-data';
providers: createControlProviders(EnumFieldComponent),
})
export class EnumFieldComponent<T> extends ValidatedFormControlSuperclass<T> {
@Input() data: MetadataFormData<string, Enums[string]>;
@Input() data: MetadataFormData<string, 'enum'>;
}

View File

@ -3,14 +3,19 @@ import { Validator, ValidationErrors, FormControl, Validators } from '@angular/f
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { FormComponentSuperclass } from '@s-libs/ng-core';
import { ThriftType } from '@vality/thrift-ts';
import { defer, switchMap, ReplaySubject, Observable } from 'rxjs';
import { defer, switchMap, ReplaySubject, Observable, combineLatest } from 'rxjs';
import { shareReplay, first, map } from 'rxjs/operators';
import { createControlProviders } from '@cc/utils';
import { ComponentChanges } from '../../../../utils';
import { MetadataFormData } from '../../types/metadata-form-data';
import { Converter } from '../../types/metadata-form-extension';
import {
Converter,
MetadataFormExtension,
MetadataFormExtensionResult,
getFirstDeterminedExtensionsResult,
} from '../../types/metadata-form-extension';
@UntilDestroy()
@Component({
@ -23,15 +28,20 @@ export class ExtensionFieldComponent<T>
implements Validator, OnChanges, OnInit
{
@Input() data: MetadataFormData<ThriftType>;
@Input() extensions: MetadataFormExtension[];
control = new FormControl<T>(null);
extensionResult$ = defer(() => this.data$).pipe(
switchMap((data) => data.extensionResult$),
extensionResult$: Observable<MetadataFormExtensionResult> = combineLatest([
defer(() => this.data$),
defer(() => this.extensions$),
]).pipe(
switchMap(([data, extensions]) => getFirstDeterminedExtensionsResult(extensions, data)),
shareReplay({ refCount: true, bufferSize: 1 })
);
private data$ = new ReplaySubject<MetadataFormData>(1);
private extensions$ = new ReplaySubject<MetadataFormExtension[]>(1);
private converter$: Observable<Converter> = this.extensionResult$.pipe(
map(
({ converter }) =>
@ -69,5 +79,6 @@ export class ExtensionFieldComponent<T>
this.data$.next(this.data);
this.control.setValidators(this.data.isRequired ? Validators.required : []);
}
if (changes.extensions) this.extensions$.next(this.extensions);
}
}

View File

@ -24,13 +24,12 @@
</ng-container>
<ng-template #input>
<div fxLayoutGap="4px">
<ng-container *ngIf="(data.extensionResult$ | async)?.type === 'datetime'; else input">
<ng-container *ngIf="(extensionResult$ | async)?.type === 'datetime'; else input">
<cc-datetime
[formControl]="control"
[hint]="aliases"
[label]="
(data.extensionResult$ | async)?.label ??
(data.type | fieldLabel: data.field)
(extensionResult$ | async)?.label ?? (data.type | fieldLabel: data.field)
"
fxFlex
></cc-datetime>
@ -49,11 +48,11 @@
<mat-form-field fxFlex>
<mat-label>
<ng-container
*ngIf="!(data.extensionResult$ | async)?.label; else extensionLabel"
*ngIf="!(extensionResult$ | async)?.label; else extensionLabel"
>{{ data.type | fieldLabel: data.field }}</ng-container
>
<ng-template #extensionLabel>{{
(data.extensionResult$ | async).label
(extensionResult$ | async).label
}}</ng-template>
</mat-label>
<mat-hint>{{ aliases }}</mat-hint>
@ -61,7 +60,7 @@
#trigger="matAutocompleteTrigger"
[formControl]="control"
[matAutocomplete]="auto"
[ngClass]="{ 'cc-code': (data.extensionResult$ | async)?.isIdentifier }"
[ngClass]="{ 'cc-code': (extensionResult$ | async)?.isIdentifier }"
[required]="data.isRequired"
[type]="inputType"
matInput
@ -86,7 +85,7 @@
</button>
</div>
<mat-autocomplete #auto="matAutocomplete">
<ng-container *ngIf="data.extensionResult$ | async as extensionResult">
<ng-container *ngIf="extensionResult$ | async as extensionResult">
<mat-option
*ngFor="let option of filteredOptions$ | async"
[value]="option.value"
@ -119,7 +118,7 @@
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<cc-json-viewer [json]="selected.details"></cc-json-viewer>
<cc-json-viewer [value]="selected.details"></cc-json-viewer>
</ng-template>
</mat-expansion-panel>
</ng-container>

View File

@ -1,10 +1,15 @@
import { Component, Input, OnChanges } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { ThriftType } from '@vality/thrift-ts';
import { combineLatest, defer, ReplaySubject, switchMap } from 'rxjs';
import { combineLatest, defer, ReplaySubject, switchMap, Observable } from 'rxjs';
import { map, pluck, shareReplay, startWith } from 'rxjs/operators';
import { ComponentChanges, getValueTypeTitle } from '@cc/app/shared';
import {
MetadataFormExtensionResult,
MetadataFormExtension,
} from '@cc/app/shared/components/metadata-form';
import { getFirstDeterminedExtensionsResult } from '@cc/app/shared/components/metadata-form/types/metadata-form-extension';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData, getAliases } from '../../types/metadata-form-data';
@ -20,9 +25,13 @@ export class PrimitiveFieldComponent<T>
implements OnChanges
{
@Input() data: MetadataFormData<ThriftType>;
@Input() extensions: MetadataFormExtension[];
extensionResult$ = defer(() => this.data$).pipe(
switchMap((data) => data.extensionResult$),
extensionResult$: Observable<MetadataFormExtensionResult> = combineLatest([
defer(() => this.data$),
defer(() => this.extensions$),
]).pipe(
switchMap(([data, extensions]) => getFirstDeterminedExtensionsResult(extensions, data)),
shareReplay({ refCount: true, bufferSize: 1 })
);
generate$ = this.extensionResult$.pipe(pluck('generate'));
@ -68,10 +77,12 @@ export class PrimitiveFieldComponent<T>
}
private data$ = new ReplaySubject<MetadataFormData<ThriftType>>(1);
private extensions$ = new ReplaySubject<MetadataFormExtension[]>(1);
ngOnChanges(changes: ComponentChanges<PrimitiveFieldComponent<T>>) {
super.ngOnChanges(changes);
if (changes.data) this.data$.next(this.data);
if (changes.extensions) this.extensions$.next(this.extensions);
}
generate(event: MouseEvent) {

View File

@ -10,7 +10,7 @@
<ng-container *ngIf="labelControl.value">
<cc-metadata-form
*ngFor="let field of data.ast"
[extensions]="data.extensions"
[extensions]="extensions"
[field]="field"
[formControl]="control.get(field.name)"
[metadata]="data.metadata"

View File

@ -2,7 +2,6 @@ import { Component, Injector, Input, OnChanges, OnInit, SimpleChanges } from '@a
import { ValidationErrors, Validators } from '@angular/forms';
import { FormBuilder } from '@ngneat/reactive-forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Field } from '@vality/thrift-ts';
import isNil from 'lodash-es/isNil';
import omitBy from 'lodash-es/omitBy';
import { merge } from 'rxjs';
@ -11,6 +10,7 @@ import { delay } from 'rxjs/operators';
import { createControlProviders, ValidatedControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data';
import { MetadataFormExtension } from '../../types/metadata-form-extension';
@UntilDestroy()
@Component({
@ -22,9 +22,10 @@ export class StructFormComponent<T extends { [N in string]: unknown }>
extends ValidatedControlSuperclass<T>
implements OnChanges, OnInit
{
@Input() data: MetadataFormData<string, Field[]>;
@Input() data: MetadataFormData<string, 'struct'>;
@Input() extensions: MetadataFormExtension[];
control = this.fb.group<T>({} as any);
control = this.fb.group<T>({} as never);
labelControl = this.fb.control(false);
get hasLabel() {

View File

@ -1,5 +1,5 @@
<cc-metadata-form
[extensions]="data.extensions"
[extensions]="extensions"
[field]="data.field"
[formControl]="control"
[metadata]="data.metadata"

View File

@ -1,9 +1,9 @@
import { Component, Input } from '@angular/core';
import { TypeDefs } from '@vality/thrift-ts';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data';
import { MetadataFormExtension } from '../../types/metadata-form-extension';
@Component({
selector: 'cc-typedef-form',
@ -11,5 +11,6 @@ import { MetadataFormData } from '../../types/metadata-form-data';
providers: createControlProviders(TypedefFormComponent),
})
export class TypedefFormComponent<T> extends ValidatedFormControlSuperclass<T> {
@Input() data: MetadataFormData<string, TypeDefs[string]>;
@Input() data: MetadataFormData<string, 'typedef'>;
@Input() extensions: MetadataFormExtension[];
}

View File

@ -21,7 +21,7 @@
</mat-form-field>
<cc-metadata-form
*ngIf="fieldControl.value"
[extensions]="data.extensions"
[extensions]="extensions"
[field]="fieldControl.value"
[formControl]="internalControl"
[metadata]="data.metadata"

View File

@ -10,6 +10,7 @@ import { delay, distinctUntilChanged, map } from 'rxjs/operators';
import { createControlProviders, getErrorsTree } from '@cc/utils';
import { MetadataFormData } from '../../types/metadata-form-data';
import { MetadataFormExtension } from '../../types/metadata-form-extension';
import { getDefaultValue } from '../../utils/get-default-value';
@UntilDestroy()
@ -22,7 +23,8 @@ export class UnionFieldComponent<T extends { [N in string]: unknown }>
extends FormComponentSuperclass<T>
implements OnInit, Validator
{
@Input() data: MetadataFormData<string, Field[]>;
@Input() data: MetadataFormData<string, 'union'>;
@Input() extensions: MetadataFormExtension[];
fieldControl = new FormControl<Field>();
internalControl = new FormControl<T[keyof T]>();

View File

@ -1,33 +1,37 @@
<div *ngIf="data" [ngSwitch]="data?.typeGroup">
<cc-extension-field
*ngIf="
(data?.extensionResult$ | async)?.type &&
(data?.extensionResult$ | async)?.type !== 'datetime';
(extensionResult$ | async)?.type && (extensionResult$ | async)?.type !== 'datetime';
else defaultFields
"
[data]="data"
[extensions]="extensions"
[formControl]="control"
></cc-extension-field>
<ng-template #defaultFields>
<cc-primitive-field
*ngSwitchCase="'primitive'"
[data]="data"
[extensions]="extensions"
[formControl]="control"
></cc-primitive-field>
<cc-complex-form
*ngSwitchCase="'complex'"
[data]="data"
[extensions]="extensions"
[formControl]="control"
></cc-complex-form>
<ng-container *ngSwitchCase="'object'" [ngSwitch]="data.objectType">
<cc-struct-form
*ngSwitchCase="'struct'"
[data]="data"
[extensions]="extensions"
[formControl]="control"
></cc-struct-form>
<cc-union-field
*ngSwitchCase="'union'"
[data]="data"
[extensions]="extensions"
[formControl]="control"
></cc-union-field>
<cc-enum-field
@ -38,6 +42,7 @@
<cc-typedef-form
*ngSwitchCase="'typedef'"
[data]="data"
[extensions]="extensions"
[formControl]="control"
></cc-typedef-form>
</ng-container>

View File

@ -1,12 +1,17 @@
import { Component, Input, OnChanges } from '@angular/core';
import { Validator } from '@angular/forms';
import { Field, ValueType } from '@vality/thrift-ts';
import { Observable } from 'rxjs';
import { ThriftAstMetadata } from '@cc/app/api/utils';
import { createControlProviders, ValidatedFormControlSuperclass } from '@cc/utils';
import { MetadataFormData } from './types/metadata-form-data';
import { MetadataFormExtension } from './types/metadata-form-extension';
import {
MetadataFormExtension,
MetadataFormExtensionResult,
getFirstDeterminedExtensionsResult,
} from './types/metadata-form-extension';
@Component({
selector: 'cc-metadata-form',
@ -25,6 +30,7 @@ export class MetadataFormComponent<T>
@Input() extensions?: MetadataFormExtension[];
data: MetadataFormData;
extensionResult$: Observable<MetadataFormExtensionResult>;
ngOnChanges() {
if (this.metadata && this.namespace && this.type) {
@ -34,8 +40,11 @@ export class MetadataFormComponent<T>
this.namespace,
this.type,
this.field,
this.parent,
this.extensions
this.parent
);
this.extensionResult$ = getFirstDeterminedExtensionsResult(
this.extensions,
this.data
);
} catch (err) {
this.data = undefined;

View File

@ -1,7 +1,5 @@
import { Field, ValueType } from '@vality/thrift-ts';
import { JsonAST } from '@vality/thrift-ts/src/thrift-parser';
import { combineLatest, Observable, switchMap } from 'rxjs';
import { map, pluck, shareReplay } from 'rxjs/operators';
import { ValuesType } from 'utility-types';
import {
@ -13,8 +11,6 @@ import {
ThriftAstMetadata,
} from '@cc/app/api/utils';
import { MetadataFormExtension, MetadataFormExtensionResult } from './metadata-form-extension';
export enum TypeGroup {
Complex = 'complex',
Primitive = 'primitive',
@ -49,24 +45,20 @@ export function isTypeWithAliases(
return Boolean(getByType(data, type, namespace));
}
type ObjectAst = ValuesType<ValuesType<ValuesType<JsonAST>>>;
export class MetadataFormData<T extends ValueType = ValueType, M extends ObjectAst = ObjectAst> {
export class MetadataFormData<
T extends ValueType = ValueType,
S extends StructureType = StructureType
> {
typeGroup: TypeGroup;
namespace: string;
type: T;
objectType?: StructureType;
ast?: M;
objectType?: S;
ast?: ValuesType<JsonAST[S]>;
include?: JsonAST['include'];
/**
* The first one identified is used
*/
extensionResult$: Observable<MetadataFormExtensionResult>;
/**
* Parent who is not typedef
*/
@ -78,6 +70,21 @@ export class MetadataFormData<T extends ValueType = ValueType, M extends ObjectA
return data;
}
/**
* Path to the object without aliases
*/
get trueTypeNode() {
const typedefs: MetadataFormData<ValueType, 'typedef'>[] = [];
let currentData: MetadataFormData = this as never;
while (currentData.objectType === 'typedef') {
typedefs.push(currentData as never);
currentData = currentData.create({
type: (currentData as MetadataFormData<ValueType, 'typedef'>).ast.type,
});
}
return { data: currentData, typedefs };
}
get isRequired() {
return this.field?.option === 'required' || this.trueParent?.objectType === 'union';
}
@ -87,26 +94,27 @@ export class MetadataFormData<T extends ValueType = ValueType, M extends ObjectA
namespace: string,
type: T,
public field?: Field,
public parent?: MetadataFormData,
public extensions?: MetadataFormExtension[]
public parent?: MetadataFormData
) {
this.setNamespaceType(namespace, type);
this.setTypeGroup();
if (this.typeGroup === TypeGroup.Object) this.setNamespaceObjectType();
}
create(params: { type?: ValueType; field?: Field }): MetadataFormData {
return new MetadataFormData(
this.metadata,
this.namespace,
params.type ?? params.field?.type,
params.field,
this as never
);
}
private setNamespaceType(namespace: string, type: T) {
const namespaceType = parseNamespaceType<T>(type, namespace);
const namespaceType = parseNamespaceType(type, namespace);
this.namespace = namespaceType.namespace;
this.type = namespaceType.type;
this.extensionResult$ = combineLatest(
(this.extensions || []).map(({ determinant }) => determinant(this))
).pipe(
map((determined) => this.extensions.filter((_, idx) => determined[idx])),
switchMap((extensions) => combineLatest(extensions.map((e) => e.extension(this)))),
pluck(0),
shareReplay({ refCount: true, bufferSize: 1 })
);
}
private setTypeGroup(type: ValueType = this.type) {
@ -124,8 +132,8 @@ export class MetadataFormData<T extends ValueType = ValueType, M extends ObjectA
this.type as string,
this.parent?.include
);
this.objectType = objectType;
this.ast = (namespaceMetadata.ast[this.objectType] as unknown)[this.type] as M;
this.objectType = objectType as never;
this.ast = (namespaceMetadata.ast[this.objectType] as unknown)[this.type] as never;
this.include = include;
}
}

View File

@ -1,5 +1,6 @@
import { ThemePalette } from '@angular/material/core';
import { Observable } from 'rxjs';
import { Observable, combineLatest, switchMap, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { MetadataFormData } from './metadata-form-data';
@ -28,3 +29,15 @@ export interface MetadataFormExtensionOption {
details?: string | object;
color?: ThemePalette;
}
export function getFirstDeterminedExtensionsResult(
sourceExtensions: MetadataFormExtension[],
data: MetadataFormData
): Observable<MetadataFormExtensionResult> {
return sourceExtensions?.length
? combineLatest(sourceExtensions.map(({ determinant }) => determinant(data))).pipe(
map((determined) => sourceExtensions.find((_, idx) => determined[idx])),
switchMap((extension) => extension?.extension(data) ?? of(null))
)
: of(null);
}

View File

@ -13,7 +13,14 @@
<mat-icon *ngIf="kind === 'editor'">view_comfy_alt</mat-icon>
</button>
</div>
<cc-json-viewer *ngIf="kind === 'component'" [json]="json"></cc-json-viewer>
<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"

View File

@ -1,8 +1,11 @@
import { Component, Input, OnChanges, Output, EventEmitter } from '@angular/core';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ValueType } from '@vality/thrift-ts';
import { ReplaySubject } from 'rxjs';
import { objectToJSON } from '../../../api/utils';
import { MetadataViewExtension } from '@cc/app/shared/components/json-viewer';
import { objectToJSON, ThriftAstMetadata } from '../../../api/utils';
import { toMonacoFile } from '../../../domain/utils';
import { ComponentChanges } from '../../utils';
@ -22,6 +25,11 @@ export class ThriftViewerComponent<T> implements OnChanges {
@Input() value: T;
@Input() compared?: T;
@Input() metadata: ThriftAstMetadata[];
@Input() namespace: string;
@Input() type: ValueType;
@Input() extensions?: MetadataViewExtension[];
@Output() changeKind = new EventEmitter<ViewerKind>();
valueFile$ = new ReplaySubject(1);
@ -31,10 +39,6 @@ export class ThriftViewerComponent<T> implements OnChanges {
return !!this.compared;
}
get json() {
return objectToJSON(this.value);
}
ngOnChanges(changes: ComponentChanges<ThriftViewerComponent<T>>) {
if (changes.value) {
this.valueFile$.next(toMonacoFile(JSON.stringify(objectToJSON(this.value), null, 2)));

View File

@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { DomainObject, Cash } from '@vality/domain-proto/lib/domain';
import { Field } from '@vality/thrift-ts';
import moment from 'moment';
import { from, Observable, of } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
@ -118,7 +117,7 @@ export class DomainMetadataFormExtensionsService {
constructor(private domainStoreService: DomainStoreService) {}
private createDomainObjectsOptions(metadata: ThriftAstMetadata[]): MetadataFormExtension[] {
const domainFields = new MetadataFormData<string, Field[]>(
const domainFields = new MetadataFormData<string, 'struct'>(
metadata,
'domain',
'DomainObject'
@ -137,7 +136,7 @@ export class DomainMetadataFormExtensionsService {
objectType: string,
objectKey: keyof DomainObject
): MetadataFormExtension {
const objectFields = new MetadataFormData<string, Field[]>(metadata, 'domain', objectType)
const objectFields = new MetadataFormData<string, 'struct'>(metadata, 'domain', objectType)
.ast;
const refType = objectFields.find((n) => n.name === 'ref').type as string;
return createDomainObjectExtension(refType, () =>

View File

@ -0,0 +1,33 @@
import { formatDate } from '@angular/common';
import { Injectable } from '@angular/core';
import { CategoryRef } from '@vality/domain-proto';
import { Timestamp } from '@vality/domain-proto/lib/base';
import { of, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { DominantCacheService } from '@cc/app/api/dominant-cache';
import { MetadataViewExtension } from '@cc/app/shared/components/json-viewer';
import { isTypeWithAliases } from '@cc/app/shared/components/metadata-form';
@Injectable({
providedIn: 'root',
})
export class DomainMetadataViewExtensionsService {
extensions$: Observable<MetadataViewExtension[]> = of([
{
determinant: (data) => of(isTypeWithAliases(data, 'CategoryRef', 'domain')),
extension: (data, value: CategoryRef) =>
this.dominantCacheService.GetCategories().pipe(
map((categories) => categories.find((c) => c.ref === String(value.id))),
map((category) => ({ value: category.name, tooltip: category }))
),
},
{
determinant: (data) => of(isTypeWithAliases(data, 'Timestamp', 'base')),
extension: (data, value: Timestamp) =>
of({ value: formatDate(value, 'dd.MM.yyyy HH:mm:ss', 'en') }),
},
]);
constructor(private dominantCacheService: DominantCacheService) {}
}

View File

@ -0,0 +1 @@
export * from './domain-metadata-view-extensions.service';

View File

@ -1,11 +1,11 @@
<div fxLayout="column" fxLayoutGap="8px">
<div class="cc-details-item-title mat-caption">{{ title }}</div>
<div class="mat-body-1 cc-details-item-content">
<div #content>
<ng-content></ng-content>
</div>
<div *ngIf="isEmpty$ | async" class="cc-details-item-content-empty">
<div *ngIf="empty; else content" class="cc-details-item-content-empty">
<mat-icon inline>hide_source</mat-icon>
</div>
<ng-template #content>
<ng-content></ng-content>
</ng-template>
</div>
</div>

View File

@ -1,41 +1,12 @@
import { ContentObserver } from '@angular/cdk/observers';
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { defer, ReplaySubject, switchMap } from 'rxjs';
import { delay, distinctUntilChanged, map, share, startWith } from 'rxjs/operators';
import { Component, Input } from '@angular/core';
import { coerceBoolean } from 'coerce-property';
@Component({
selector: 'cc-details-item',
templateUrl: 'details-item.component.html',
styleUrls: ['details-item.component.scss'],
})
export class DetailsItemComponent implements AfterViewInit {
export class DetailsItemComponent {
@Input() title: string;
@ViewChild('content') contentElementRef: ElementRef<HTMLElement>;
isEmpty$ = defer(() => this.viewInit$).pipe(
switchMap(() =>
this.contentObserver.observe(this.contentElementRef.nativeElement).pipe(startWith(null))
),
map(() => this.getIsEmpty()),
distinctUntilChanged(),
delay(0),
share()
);
private viewInit$ = new ReplaySubject<void>(1);
constructor(private contentObserver: ContentObserver) {}
ngAfterViewInit() {
this.viewInit$.next();
}
private getIsEmpty() {
return !Array.from(this.contentElementRef.nativeElement.childNodes).find(
(n) =>
n.nodeType !== Node.COMMENT_NODE ||
(n.nodeType === Node.TEXT_NODE && n.nodeValue.trim())
);
}
@Input() @coerceBoolean empty: boolean;
}