mirror of
https://github.com/valitydev/control-center.git
synced 2024-11-06 02:25:17 +00:00
TD-449: Support thrift viewer ext (#161)
This commit is contained in:
parent
ae2fff4ba1
commit
7136470378
48
package-lock.json
generated
48
package-lock.json
generated
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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));
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
export * from './clean';
|
||||
export * from './inline-json';
|
||||
export * from './compare-different-types';
|
||||
export * from './split-ids';
|
||||
export * from './is-empty';
|
||||
|
7
projects/ng-core/src/lib/utils/is-empty.ts
Normal file
7
projects/ng-core/src/lib/utils/is-empty.ts
Normal 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 === '';
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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>
|
@ -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) {}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './shop-main-info.module';
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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 {}
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
@ -0,0 +1,3 @@
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
@ -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'));
|
||||
}
|
||||
}
|
@ -1,2 +1,2 @@
|
||||
export * from './json-viewer.module';
|
||||
export * from './types/patch';
|
||||
export * from './utils/metadata-view-extension';
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
5
src/app/shared/components/json-viewer/json-viewer.scss
Normal file
5
src/app/shared/components/json-viewer/json-viewer.scss
Normal file
@ -0,0 +1,5 @@
|
||||
::ng-deep .tooltip {
|
||||
display: block;
|
||||
unicode-bidi: embed;
|
||||
white-space: pre;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
export interface Patch {
|
||||
path: string[];
|
||||
key?: string;
|
||||
value?: unknown;
|
||||
tooltip?: string;
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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];
|
||||
}
|
@ -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);
|
||||
}
|
190
src/app/shared/components/json-viewer/utils/metadata-view.ts
Normal file
190
src/app/shared/components/json-viewer/utils/metadata-view.ts
Normal 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))
|
||||
)
|
||||
: [];
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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([]);
|
||||
|
@ -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'>;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
@ -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() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<cc-metadata-form
|
||||
[extensions]="data.extensions"
|
||||
[extensions]="extensions"
|
||||
[field]="data.field"
|
||||
[formControl]="control"
|
||||
[metadata]="data.metadata"
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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]>();
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)));
|
||||
|
@ -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, () =>
|
||||
|
@ -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) {}
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './domain-metadata-view-extensions.service';
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user