mirror of
https://github.com/valitydev/control-center.git
synced 2024-11-06 02:25:17 +00:00
IMP-29: Add new claims modifications manager (#82)
This commit is contained in:
parent
ee208676be
commit
5969598e72
@ -31,6 +31,7 @@
|
||||
"thrift-ts",
|
||||
"buffer",
|
||||
"humanize-duration",
|
||||
"node-int64",
|
||||
"@vality/deanonimus-proto",
|
||||
"@vality/domain-proto",
|
||||
"@vality/dominant-cache-proto",
|
||||
|
@ -6,8 +6,7 @@ import {
|
||||
isPrimitiveType,
|
||||
isThriftObject,
|
||||
parseNamespaceType,
|
||||
StructureType,
|
||||
STRUCTURE_TYPES,
|
||||
parseNamespaceObjectType,
|
||||
} from './namespace-type';
|
||||
import { ThriftAstMetadata, ThriftInstanceContext } from './types';
|
||||
|
||||
@ -52,26 +51,17 @@ export function createThriftInstance<V>(
|
||||
return value;
|
||||
}
|
||||
}
|
||||
const namespaceMeta = metadata.find((m) => m.name === namespace);
|
||||
const structureType = (Object.keys(namespaceMeta.ast) as StructureType[]).find(
|
||||
(t) => namespaceMeta.ast[t][type]
|
||||
);
|
||||
if (!structureType || !STRUCTURE_TYPES.includes(structureType)) {
|
||||
throw new Error('Unknown thrift structure type');
|
||||
}
|
||||
switch (structureType) {
|
||||
const { namespaceMetadata, objectType } = parseNamespaceObjectType(metadata, namespace, type);
|
||||
switch (objectType) {
|
||||
case 'enum':
|
||||
return value;
|
||||
case 'exception':
|
||||
throw new Error('Unsupported structure type: exception');
|
||||
default: {
|
||||
const typeMeta = namespaceMeta.ast[structureType][type];
|
||||
const typeMeta = namespaceMetadata.ast[objectType][type];
|
||||
try {
|
||||
if (structureType === 'typedef') {
|
||||
type TypedefType = {
|
||||
type: ValueType;
|
||||
};
|
||||
const typedefMeta = (typeMeta as TypedefType).type;
|
||||
if (objectType === 'typedef') {
|
||||
const typedefMeta = (typeMeta as { type: ValueType }).type;
|
||||
return internalCreateThriftInstance(typedefMeta, value);
|
||||
}
|
||||
const instance = new instanceContext[namespace][type]();
|
||||
@ -84,7 +74,7 @@ export function createThriftInstance<V>(
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Thrift structure',
|
||||
structureType,
|
||||
objectType,
|
||||
'creation error:',
|
||||
namespace,
|
||||
type,
|
||||
|
@ -1,5 +1,7 @@
|
||||
import type { ListType, MapType, SetType, ThriftType, ValueType } from '@vality/thrift-ts';
|
||||
|
||||
import { ThriftAstMetadata } from '@cc/app/api/utils';
|
||||
|
||||
export const PRIMITIVE_TYPES = [
|
||||
'int',
|
||||
'bool',
|
||||
@ -21,15 +23,43 @@ export function isComplexType(type: ValueType): type is SetType | ListType | Map
|
||||
}
|
||||
|
||||
export function isPrimitiveType(type: ValueType): type is ThriftType {
|
||||
return PRIMITIVE_TYPES.includes(type as any);
|
||||
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 function parseNamespaceType(type: ValueType, currentNamespace?: string) {
|
||||
if (!isComplexType(type) && !isPrimitiveType(type) && type.includes('.')) {
|
||||
[currentNamespace, type] = type.split('.');
|
||||
}
|
||||
return { namespace: currentNamespace, type };
|
||||
export interface NamespaceObjectType {
|
||||
namespaceMetadata: ThriftAstMetadata;
|
||||
objectType: StructureType;
|
||||
}
|
||||
|
||||
export function parseNamespaceObjectType(
|
||||
metadata: ThriftAstMetadata[],
|
||||
namespace: string,
|
||||
type: string
|
||||
): NamespaceObjectType {
|
||||
const namespaceMetadata = metadata.find((m) => m.name === namespace);
|
||||
const objectType = (Object.keys(namespaceMetadata.ast) as StructureType[]).find(
|
||||
(t) => namespaceMetadata.ast[t][type]
|
||||
);
|
||||
if (!objectType || !STRUCTURE_TYPES.includes(objectType)) {
|
||||
throw new Error(`Unknown thrift structure type: ${objectType}`);
|
||||
}
|
||||
return { namespaceMetadata, objectType };
|
||||
}
|
||||
|
||||
export interface NamespaceType<T extends ValueType = ValueType> {
|
||||
namespace: string;
|
||||
type: T;
|
||||
}
|
||||
|
||||
export function parseNamespaceType<T extends ValueType>(
|
||||
type: T,
|
||||
namespace?: string
|
||||
): NamespaceType<T> {
|
||||
if (!isPrimitiveType(type) && !isComplexType(type) && type.includes('.')) {
|
||||
[namespace, type as unknown] = type.split('.');
|
||||
}
|
||||
return { namespace, type };
|
||||
}
|
||||
|
@ -5,8 +5,7 @@ import {
|
||||
isComplexType,
|
||||
isPrimitiveType,
|
||||
parseNamespaceType,
|
||||
StructureType,
|
||||
STRUCTURE_TYPES,
|
||||
parseNamespaceObjectType,
|
||||
} from './namespace-type';
|
||||
import { ThriftAstMetadata } from './types';
|
||||
|
||||
@ -52,15 +51,9 @@ export function thriftInstanceToObject<V>(
|
||||
return value;
|
||||
}
|
||||
}
|
||||
const namespaceMeta = metadata.find((m) => m.name === namespace);
|
||||
const structureType = (Object.keys(namespaceMeta.ast) as StructureType[]).find(
|
||||
(t) => namespaceMeta.ast[t][type]
|
||||
);
|
||||
if (!structureType || !STRUCTURE_TYPES.includes(structureType)) {
|
||||
throw new Error(`Unknown thrift structure type: ${structureType}`);
|
||||
}
|
||||
const typeMeta = namespaceMeta.ast[structureType][type];
|
||||
switch (structureType) {
|
||||
const { namespaceMetadata, objectType } = parseNamespaceObjectType(metadata, namespace, type);
|
||||
const typeMeta = namespaceMetadata.ast[objectType][type];
|
||||
switch (objectType) {
|
||||
case 'exception':
|
||||
throw new Error('Unsupported structure type: exception');
|
||||
case 'typedef': {
|
||||
|
@ -93,5 +93,14 @@
|
||||
</ng-container>
|
||||
</cc-timeline>
|
||||
</ng-template>
|
||||
<mat-card>
|
||||
<mat-card-content fxLayoutGap="24px">
|
||||
<button mat-button color="primary" (click)="addModification()">
|
||||
ADD MODIFICATION
|
||||
</button>
|
||||
<!-- <button mat-button>ATTACH FILE</button>-->
|
||||
<!-- <button mat-button>CHANGE STATUS</button>-->
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,15 +1,29 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable, switchMap, EMPTY, BehaviorSubject, merge } from 'rxjs';
|
||||
import { shareReplay, catchError } from 'rxjs/operators';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import {
|
||||
Observable,
|
||||
switchMap,
|
||||
EMPTY,
|
||||
BehaviorSubject,
|
||||
merge,
|
||||
combineLatest,
|
||||
Subject,
|
||||
defer,
|
||||
} from 'rxjs';
|
||||
import { shareReplay, catchError, map, first } from 'rxjs/operators';
|
||||
|
||||
import { ClaimManagementService } from '@cc/app/api/claim-management';
|
||||
import { PartyManagementWithUserService } from '@cc/app/api/payment-processing';
|
||||
import { NotificationService } from '@cc/app/shared/services/notification';
|
||||
import { DIALOG_CONFIG, DialogConfig } from '@cc/app/tokens';
|
||||
import { inProgressFrom, progressTo } from '@cc/utils';
|
||||
|
||||
import { AddModificationDialogComponent } from './components/add-modification-dialog/add-modification-dialog.component';
|
||||
import { CLAIM_STATUS_COLOR } from './types/claim-status-color';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'cc-claim',
|
||||
templateUrl: './claim.component.html',
|
||||
@ -29,8 +43,12 @@ export class ClaimComponent {
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 })
|
||||
);
|
||||
claim$ = (this.route.params as Observable<Record<string, string>>).pipe(
|
||||
switchMap(({ claimID, partyID }) =>
|
||||
claim$ = merge(
|
||||
this.route.params,
|
||||
defer(() => this.loadClaim$)
|
||||
).pipe(
|
||||
map(() => this.route.snapshot.params as Record<string, string>),
|
||||
switchMap(({ partyID, claimID }) =>
|
||||
this.claimManagementService.GetClaim(partyID, Number(claimID)).pipe(
|
||||
progressTo(this.progress$),
|
||||
catchError((err) => {
|
||||
@ -46,11 +64,33 @@ export class ClaimComponent {
|
||||
statusColor = CLAIM_STATUS_COLOR;
|
||||
|
||||
private progress$ = new BehaviorSubject(0);
|
||||
private loadClaim$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private claimManagementService: ClaimManagementService,
|
||||
private partyManagementWithUserService: PartyManagementWithUserService,
|
||||
private notificationService: NotificationService
|
||||
private notificationService: NotificationService,
|
||||
private dialog: MatDialog,
|
||||
@Inject(DIALOG_CONFIG) private dialogConfig: DialogConfig
|
||||
) {}
|
||||
|
||||
addModification() {
|
||||
combineLatest([this.party$, this.claim$])
|
||||
.pipe(
|
||||
first(),
|
||||
switchMap(([party, claim]) =>
|
||||
this.dialog
|
||||
.open(AddModificationDialogComponent, {
|
||||
...this.dialogConfig.large,
|
||||
data: { party, claim },
|
||||
})
|
||||
.afterClosed()
|
||||
),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((result) => {
|
||||
if (result === 'success') this.loadClaim$.next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,19 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { ShopModificationTimelineItemComponent } from '@cc/app/sections/claim/components/shop-modification-timeline-item/shop-modification-timeline-item.component';
|
||||
import { StatusModule } from '@cc/app/shared/components';
|
||||
import { MetadataFormModule, StatusModule } from '@cc/app/shared/components';
|
||||
import { JsonViewerModule } from '@cc/app/shared/components/json-viewer/json-viewer.module';
|
||||
import { ThriftPipesModule } from '@cc/app/shared/pipes';
|
||||
import { TimelineModule } from '@cc/components/timeline';
|
||||
@ -17,8 +21,10 @@ import { TimelineModule } from '@cc/components/timeline';
|
||||
import { TimelineComponentsModule } from '../party-claim/changeset/timeline-components';
|
||||
import { ClaimRoutingModule } from './claim-routing.module';
|
||||
import { ClaimComponent } from './claim.component';
|
||||
import { AddModificationDialogComponent } from './components/add-modification-dialog/add-modification-dialog.component';
|
||||
import { CommentModificationTimelineItemComponent } from './components/comment-modification-timeline-item/comment-modification-timeline-item.component';
|
||||
import { ModificationUnitTimelineItemComponent } from './components/modification-unit-timeline-item/modification-unit-timeline-item.component';
|
||||
import { ShopModificationTimelineItemComponent } from './components/shop-modification-timeline-item/shop-modification-timeline-item.component';
|
||||
import { StatusModificationTimelineItemComponent } from './components/status-modification-timeline-item/status-modification-timeline-item.component';
|
||||
|
||||
@NgModule({
|
||||
@ -28,6 +34,7 @@ import { StatusModificationTimelineItemComponent } from './components/status-mod
|
||||
StatusModificationTimelineItemComponent,
|
||||
CommentModificationTimelineItemComponent,
|
||||
ShopModificationTimelineItemComponent,
|
||||
AddModificationDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -44,6 +51,12 @@ import { StatusModificationTimelineItemComponent } from './components/status-mod
|
||||
StatusModule,
|
||||
MatDividerModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatButtonModule,
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatInputModule,
|
||||
MetadataFormModule,
|
||||
],
|
||||
})
|
||||
export class ClaimModule {}
|
||||
|
@ -0,0 +1,24 @@
|
||||
<div fxLayout="column" fxLayoutGap="32px">
|
||||
<div class="cc-headline">Add modification unit</div>
|
||||
|
||||
<cc-metadata-form
|
||||
[formControl]="control"
|
||||
[metadata]="metadata$ | async"
|
||||
[extensions]="extensions"
|
||||
[disabled]="!!(progress$ | async)"
|
||||
namespace="claim_management"
|
||||
type="Modification"
|
||||
></cc-metadata-form>
|
||||
|
||||
<div fxLayout fxLayoutAlign="space-between">
|
||||
<button mat-button (click)="cancel()" [disabled]="!!(progress$ | async)">CANCEL</button>
|
||||
<button
|
||||
mat-button
|
||||
color="primary"
|
||||
(click)="add()"
|
||||
[disabled]="control.invalid || !!(progress$ | async)"
|
||||
>
|
||||
ADD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,150 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { FormBuilder } from '@ngneat/reactive-forms';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Claim, Modification } from '@vality/domain-proto/lib/claim_management';
|
||||
import { Party } from '@vality/domain-proto/lib/domain';
|
||||
import uniqBy from 'lodash-es/uniqBy';
|
||||
import { BehaviorSubject, from, of } from 'rxjs';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { ClaimManagementService } from '@cc/app/api/claim-management';
|
||||
import { getByType, MetadataFormExtension } from '@cc/app/shared/components/metadata-form';
|
||||
import { NotificationService } from '@cc/app/shared/services/notification';
|
||||
import { progressTo } from '@cc/utils';
|
||||
|
||||
function createPartyOptions(values: IterableIterator<{ id: string }>) {
|
||||
return Array.from(values).map((value) => ({
|
||||
label: `${value.id} (from party)`,
|
||||
details: value,
|
||||
value: value.id,
|
||||
}));
|
||||
}
|
||||
|
||||
function createClaimOptions(modificationUnits: { id: string; modification: unknown }[]) {
|
||||
return uniqBy(
|
||||
modificationUnits.filter(Boolean).map((unit) => ({
|
||||
label: `${unit.id} (from claim)`,
|
||||
details: unit.modification,
|
||||
value: unit.id,
|
||||
})),
|
||||
'value'
|
||||
);
|
||||
}
|
||||
|
||||
function generate() {
|
||||
return of(uuid());
|
||||
}
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'cc-add-modification-dialog',
|
||||
templateUrl: './add-modification-dialog.component.html',
|
||||
})
|
||||
export class AddModificationDialogComponent {
|
||||
control = this.fb.control<Modification>(null);
|
||||
metadata$ = from(import('@vality/domain-proto/lib/metadata.json').then((m) => m.default));
|
||||
extensions: MetadataFormExtension[] = [
|
||||
{
|
||||
determinant: (data) => of(!!getByType(data, 'ContractorID', 'domain')),
|
||||
extension: () =>
|
||||
of({
|
||||
options: [
|
||||
...createPartyOptions(this.dialogData.party.contractors.values()),
|
||||
...createClaimOptions(
|
||||
this.dialogData.claim.changeset.map(
|
||||
(unit) =>
|
||||
unit.modification.party_modification?.contractor_modification
|
||||
)
|
||||
),
|
||||
],
|
||||
generate,
|
||||
}),
|
||||
},
|
||||
{
|
||||
determinant: (data) => of(!!getByType(data, 'ContractID', 'domain')),
|
||||
extension: () =>
|
||||
of({
|
||||
options: [
|
||||
...createPartyOptions(this.dialogData.party.contracts.values()),
|
||||
...createClaimOptions(
|
||||
this.dialogData.claim.changeset.map(
|
||||
(unit) =>
|
||||
unit.modification.party_modification?.contract_modification
|
||||
)
|
||||
),
|
||||
],
|
||||
generate,
|
||||
}),
|
||||
},
|
||||
{
|
||||
determinant: (data) => of(!!getByType(data, 'ShopID', 'domain')),
|
||||
extension: () =>
|
||||
of({
|
||||
options: [
|
||||
...createPartyOptions(this.dialogData.party.shops.values()),
|
||||
...createClaimOptions(
|
||||
this.dialogData.claim.changeset.map(
|
||||
(unit) => unit.modification.party_modification?.shop_modification
|
||||
)
|
||||
),
|
||||
],
|
||||
generate,
|
||||
}),
|
||||
},
|
||||
{
|
||||
determinant: (data) => of(!!getByType(data, 'WalletID', 'domain')),
|
||||
extension: () =>
|
||||
of({
|
||||
options: [
|
||||
...createPartyOptions(this.dialogData.party.wallets.values()),
|
||||
...createClaimOptions(
|
||||
this.dialogData.claim.changeset.map(
|
||||
(unit) => unit.modification.party_modification?.wallet_modification
|
||||
)
|
||||
),
|
||||
],
|
||||
generate,
|
||||
}),
|
||||
},
|
||||
{
|
||||
determinant: (data) => of(!!getByType(data, 'ID', 'base')),
|
||||
extension: () => of({ generate }),
|
||||
},
|
||||
];
|
||||
progress$ = new BehaviorSubject(0);
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private dialogRef: MatDialogRef<AddModificationDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA)
|
||||
private dialogData: { party: Party; claim: Claim },
|
||||
private claimManagementService: ClaimManagementService,
|
||||
private notificationService: NotificationService
|
||||
) {}
|
||||
|
||||
add() {
|
||||
this.claimManagementService
|
||||
.UpdateClaim(
|
||||
this.dialogData.party.id,
|
||||
this.dialogData.claim.id,
|
||||
this.dialogData.claim.revision,
|
||||
[this.control.value]
|
||||
)
|
||||
.pipe(progressTo(this.progress$), untilDestroyed(this))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.notificationService.success('Modification added successfully');
|
||||
this.dialogRef.close('success');
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(err);
|
||||
this.notificationService.error('Error adding modification');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ import { ClaimStatus } from '@vality/domain-proto/lib/claim_management';
|
||||
import { StatusColor } from '@cc/app/styles';
|
||||
|
||||
export const CLAIM_STATUS_COLOR: Record<keyof ClaimStatus, StatusColor> = {
|
||||
pending_acceptance: StatusColor.Success,
|
||||
pending_acceptance: null,
|
||||
accepted: StatusColor.Success,
|
||||
pending: StatusColor.Pending,
|
||||
review: StatusColor.Pending,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<span [ngClass]="{ 'cc-body-2': true, outdated: outdated, removed: removed }">{{ username }}</span>
|
||||
<span [ngClass]="{ 'cc-body-1': true, outdated: outdated, removed: removed }">
|
||||
<span [ngClass]="{ outdated: outdated, removed: removed }">{{ username }}</span>
|
||||
<span [ngClass]="{ outdated: outdated, removed: removed }">
|
||||
{{ text }}
|
||||
<span *ngIf="createdAt"
|
||||
>at {{ createdAt | date: 'dd.MM.yyyy HH:mm:ss' }} ({{
|
||||
@ -7,5 +7,5 @@
|
||||
}})</span
|
||||
>
|
||||
</span>
|
||||
<span class="cc-body-1" *ngIf="outdated"> (Outdated)</span>
|
||||
<span class="cc-body-1" *ngIf="removed"> (Removed)</span>
|
||||
<span *ngIf="outdated"> (Outdated)</span>
|
||||
<span *ngIf="removed"> (Removed)</span>
|
||||
|
@ -9,3 +9,4 @@ export * from './shop-field';
|
||||
export * from './shop-details';
|
||||
export * from './payout-tool-details';
|
||||
export * from './payout-tool-field';
|
||||
export * from './metadata-form';
|
||||
|
@ -5,7 +5,7 @@
|
||||
[title]="item.key | keyTitle | titlecase"
|
||||
>
|
||||
<span
|
||||
style="cursor: default"
|
||||
*ngIf="!!item.value"
|
||||
[matTooltip]="item.tooltip"
|
||||
fxLayoutGap="4px"
|
||||
fxLayoutAlign=" center"
|
||||
|
@ -1,4 +1,6 @@
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import isNil from 'lodash-es/isNil';
|
||||
import isObject from 'lodash-es/isObject';
|
||||
|
||||
import { Patch } from '../types/patch';
|
||||
|
||||
@ -20,7 +22,7 @@ export class InlineItem {
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return isEmpty(this.sourceValue);
|
||||
return isObject(this.sourceValue) ? isEmpty(this.sourceValue) : isNil(this.sourceValue);
|
||||
}
|
||||
|
||||
constructor(public path: string[], public sourceValue: unknown, private patch?: Patch) {
|
||||
|
@ -0,0 +1,51 @@
|
||||
<div gdColumns="1fr" gdGap="16px">
|
||||
<span class="cc-body-2"
|
||||
>{{ data.field?.name | keyTitle | titlecase }} ({{ data.type.name | titlecase }})</span
|
||||
>
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel
|
||||
class="mat-elevation-z0"
|
||||
*ngFor="let control of controls.controls; let i = index"
|
||||
>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title fxLayoutAlign=" center">
|
||||
{{ i + 1 }}.
|
||||
{{
|
||||
data.type.name === 'map'
|
||||
? (data.type.keyType | valueTypeTitle | titlecase) + ' - '
|
||||
: ''
|
||||
}}
|
||||
{{ data.type.valueType | valueTypeTitle | titlecase }}
|
||||
</mat-panel-title>
|
||||
<mat-panel-description fxLayoutAlign="end">
|
||||
<button mat-icon-button (click)="delete(i)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<div gdColumns="1fr" gdGap="16px">
|
||||
<ng-container *ngIf="data.type.name === 'map'">
|
||||
<span class="cc-body-2">Key</span>
|
||||
<cc-metadata-form
|
||||
[metadata]="data.metadata"
|
||||
[namespace]="data.namespace"
|
||||
[type]="data.type.keyType"
|
||||
[parent]="data"
|
||||
[extensions]="data.extensions"
|
||||
></cc-metadata-form>
|
||||
</ng-container>
|
||||
<span class="cc-body-2" *ngIf="data.type.name === 'map'">Value</span>
|
||||
<cc-metadata-form
|
||||
[metadata]="data.metadata"
|
||||
[namespace]="data.namespace"
|
||||
[type]="data.type.valueType"
|
||||
[parent]="data"
|
||||
[extensions]="data.extensions"
|
||||
></cc-metadata-form>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
<button mat-button (click)="add()">
|
||||
ADD {{ data.type.valueType | valueTypeTitle | uppercase }}
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,8 @@
|
||||
mat-expansion-panel-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
mat-expansion-panel-header {
|
||||
padding: 0 2px 0 0;
|
||||
background: #fff !important;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ValidationErrors, Validator } from '@angular/forms';
|
||||
import { FormArray, FormControl } from '@ngneat/reactive-forms';
|
||||
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
|
||||
import { MapType, SetType, ListType } from '@vality/thrift-ts';
|
||||
|
||||
import { createValidatedAbstractControlProviders } from '@cc/utils';
|
||||
|
||||
import { MetadataFormData } from '../../types/metadata-form-data';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-complex-form',
|
||||
templateUrl: './complex-form.component.html',
|
||||
styleUrls: ['complex-form.component.scss'],
|
||||
providers: createValidatedAbstractControlProviders(ComplexFormComponent),
|
||||
})
|
||||
export class ComplexFormComponent
|
||||
extends WrappedFormControlSuperclass<unknown>
|
||||
implements Validator
|
||||
{
|
||||
@Input() data: MetadataFormData<SetType | MapType | ListType>;
|
||||
|
||||
controls = new FormArray([]);
|
||||
|
||||
add() {
|
||||
this.controls.push(new FormControl());
|
||||
}
|
||||
|
||||
delete(idx: number) {
|
||||
this.controls.removeAt(idx);
|
||||
}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.control.invalid || this.controls.invalid ? { invalid: true } : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<mat-form-field style="width: 100%">
|
||||
<mat-label>{{ data.field?.name || data.type | keyTitle | titlecase }}</mat-label>
|
||||
<mat-select [formControl]="control" [required]="data.field?.option === 'required'">
|
||||
<mat-option *ngFor="let item of data.ast.items" [value]="item">{{ item.name }}</mat-option>
|
||||
</mat-select>
|
||||
<button
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
*ngIf="control.value && data.field?.option !== 'required'"
|
||||
(click)="control.reset(); $event.stopPropagation()"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
@ -0,0 +1,21 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ValidationErrors, Validator } from '@angular/forms';
|
||||
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
|
||||
import { Enums } from '@vality/thrift-ts/src/thrift-parser';
|
||||
|
||||
import { createValidatedAbstractControlProviders } from '@cc/utils';
|
||||
|
||||
import { MetadataFormData } from '../../types/metadata-form-data';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-enum-field',
|
||||
templateUrl: './enum-field.component.html',
|
||||
providers: createValidatedAbstractControlProviders(EnumFieldComponent),
|
||||
})
|
||||
export class EnumFieldComponent extends WrappedFormControlSuperclass<unknown> implements Validator {
|
||||
@Input() data: MetadataFormData<string, Enums[string]>;
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.control.invalid ? { invalid: true } : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
<div gdColumns="1fr" gdGap="16px">
|
||||
<div fxLayoutGap="4px">
|
||||
<mat-form-field fxFlex>
|
||||
<mat-label>{{
|
||||
data.field
|
||||
? (data.field.name | keyTitle | titlecase)
|
||||
: (data.typedefData.type | valueTypeTitle | titlecase)
|
||||
}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[matAutocomplete]="auto"
|
||||
#trigger="matAutocompleteTrigger"
|
||||
[formControl]="control"
|
||||
[required]="data.field?.option === 'required'"
|
||||
/>
|
||||
<div matSuffix fxLayoutGap="4px">
|
||||
<button mat-icon-button *ngIf="control.value" (click)="clear($event)">
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="(extensionResult$ | async)?.options?.length"
|
||||
mat-icon-button
|
||||
(click)="
|
||||
auto.isOpen ? trigger.closePanel() : trigger.openPanel();
|
||||
$event.stopPropagation()
|
||||
"
|
||||
>
|
||||
<mat-icon>{{ auto.isOpen ? 'expand_less' : 'expand_more' }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<mat-autocomplete #auto="matAutocomplete">
|
||||
<ng-container *ngIf="data.extensionResult$ | async as extensionResult">
|
||||
<mat-option
|
||||
*ngFor="let option of extensionResult.options"
|
||||
[value]="option.value"
|
||||
>
|
||||
{{ option.label || option.value }}
|
||||
</mat-option>
|
||||
</ng-container>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
<button *ngIf="generate$ | async" mat-icon-button (click)="generate($event)">
|
||||
<mat-icon>loop</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="selected$ | async as selected">
|
||||
<mat-expansion-panel *ngIf="selected.details">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{ selected.label }}</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<cc-json-viewer [json]="selected.details"></cc-json-viewer>
|
||||
</mat-expansion-panel>
|
||||
</ng-container>
|
||||
</div>
|
@ -0,0 +1,4 @@
|
||||
::ng-deep .mat-tooltip {
|
||||
font-family: monospace !important;
|
||||
white-space: pre-line !important;
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
import { Component, Input, OnChanges } from '@angular/core';
|
||||
import { ValidationErrors, Validator } from '@angular/forms';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
|
||||
import { ThriftType } from '@vality/thrift-ts';
|
||||
import { combineLatest, defer, ReplaySubject, switchMap } from 'rxjs';
|
||||
import { map, pluck, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import { ComponentChanges } from '@cc/app/shared';
|
||||
import { createValidatedAbstractControlProviders } from '@cc/utils';
|
||||
|
||||
import { MetadataFormData } from '../../types/metadata-form-data';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'cc-primitive-field',
|
||||
templateUrl: './primitive-field.component.html',
|
||||
styleUrls: ['primitive-field.component.scss'],
|
||||
providers: createValidatedAbstractControlProviders(PrimitiveFieldComponent),
|
||||
})
|
||||
export class PrimitiveFieldComponent
|
||||
extends WrappedFormControlSuperclass<unknown>
|
||||
implements OnChanges, Validator
|
||||
{
|
||||
@Input() data: MetadataFormData<ThriftType>;
|
||||
|
||||
extensionResult$ = defer(() => this.data$).pipe(
|
||||
switchMap((data) => data.extensionResult$),
|
||||
shareReplay({ refCount: true, bufferSize: 1 })
|
||||
);
|
||||
generate$ = this.extensionResult$.pipe(pluck('generate'));
|
||||
selected$ = combineLatest([this.extensionResult$, this.control.valueChanges]).pipe(
|
||||
map(([extensionResult, value]) => extensionResult.options.find((o) => o.value === value))
|
||||
);
|
||||
|
||||
private data$ = new ReplaySubject<MetadataFormData<ThriftType>>(1);
|
||||
|
||||
ngOnChanges(changes: ComponentChanges<PrimitiveFieldComponent>) {
|
||||
super.ngOnChanges(changes);
|
||||
if (changes.data) this.data$.next(this.data);
|
||||
}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.control.invalid ? { invalid: true } : null;
|
||||
}
|
||||
|
||||
generate(event: MouseEvent) {
|
||||
this.generate$
|
||||
.pipe(
|
||||
switchMap((generate) => generate()),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((value) => this.control.setValue(value));
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
clear(event: MouseEvent) {
|
||||
this.control.reset(null);
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<div gdColumns="1fr" gdGap="16px" style="padding-left: 16px">
|
||||
<div *ngIf="hasLabel" class="cc-body-1">
|
||||
{{ data.field?.name | keyTitle | titlecase }}
|
||||
</div>
|
||||
<cc-metadata-form
|
||||
*ngFor="let field of data.ast"
|
||||
[formControl]="control.get(field.name)"
|
||||
[metadata]="data.metadata"
|
||||
[namespace]="data.namespace"
|
||||
[type]="field.type"
|
||||
[field]="field"
|
||||
[parent]="data"
|
||||
[extensions]="data.extensions"
|
||||
></cc-metadata-form>
|
||||
</div>
|
@ -0,0 +1,51 @@
|
||||
import { Component, Injector, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { ValidationErrors, Validator } from '@angular/forms';
|
||||
import { FormBuilder, FormControl } from '@ngneat/reactive-forms';
|
||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||
import { WrappedControlSuperclass } from '@s-libs/ng-core';
|
||||
import { Field } from '@vality/thrift-ts';
|
||||
|
||||
import { createValidatedAbstractControlProviders } from '@cc/utils';
|
||||
|
||||
import { MetadataFormData } from '../../types/metadata-form-data';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'cc-struct-form',
|
||||
templateUrl: './struct-form.component.html',
|
||||
providers: createValidatedAbstractControlProviders(StructFormComponent),
|
||||
})
|
||||
export class StructFormComponent
|
||||
extends WrappedControlSuperclass<{ [N in string]: unknown }>
|
||||
implements OnChanges, Validator
|
||||
{
|
||||
@Input() data: MetadataFormData<string, Field[]>;
|
||||
|
||||
control = this.fb.group<{ [N in string]: unknown }>({});
|
||||
|
||||
constructor(injector: Injector, private fb: FormBuilder) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
super.ngOnChanges(changes);
|
||||
const newControlsNames = new Set(this.data.ast.map(({ name }) => name));
|
||||
Object.keys(this.control.controls).forEach((name) => {
|
||||
if (newControlsNames.has(name)) newControlsNames.delete(name);
|
||||
else this.control.removeControl(name);
|
||||
});
|
||||
newControlsNames.forEach((name) => this.control.addControl(name, new FormControl()));
|
||||
}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.control.invalid ? { invalid: true } : null;
|
||||
}
|
||||
|
||||
get hasLabel() {
|
||||
let parent: MetadataFormData = this.data.parent;
|
||||
while (parent?.objectType === 'typedef') {
|
||||
parent = parent?.parent;
|
||||
}
|
||||
return parent?.objectType !== 'union' && this.data.field;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<cc-metadata-form
|
||||
[formControl]="control"
|
||||
[metadata]="data.metadata"
|
||||
[namespace]="data.namespace"
|
||||
[type]="data.ast.type"
|
||||
[field]="data.field"
|
||||
[parent]="data"
|
||||
[extensions]="data.extensions"
|
||||
></cc-metadata-form>
|
@ -0,0 +1,24 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ValidationErrors, Validator } from '@angular/forms';
|
||||
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
|
||||
import { TypeDefs } from '@vality/thrift-ts';
|
||||
|
||||
import { createValidatedAbstractControlProviders } from '@cc/utils';
|
||||
|
||||
import { MetadataFormData } from '../../types/metadata-form-data';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-typedef-form',
|
||||
templateUrl: './typedef-form.component.html',
|
||||
providers: createValidatedAbstractControlProviders(TypedefFormComponent),
|
||||
})
|
||||
export class TypedefFormComponent
|
||||
extends WrappedFormControlSuperclass<unknown>
|
||||
implements Validator
|
||||
{
|
||||
@Input() data: MetadataFormData<string, TypeDefs[string]>;
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.control.invalid ? { invalid: true } : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
<div gdColumns="1fr" gdGap="16px">
|
||||
<mat-form-field style="width: 100%">
|
||||
<mat-label>{{ data.field?.name || data.type | keyTitle | titlecase }}</mat-label>
|
||||
<mat-select [formControl]="fieldControl" [required]="data.field?.option === 'required'">
|
||||
<mat-option *ngFor="let field of data.ast" [value]="field">{{
|
||||
field.name | keyTitle | titlecase
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
<button
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
*ngIf="fieldControl.value && data.field?.option !== 'required'"
|
||||
(click)="fieldControl.reset(); $event.stopPropagation()"
|
||||
>
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<cc-metadata-form
|
||||
*ngIf="fieldControl.value"
|
||||
[formControl]="internalControl"
|
||||
[metadata]="data.metadata"
|
||||
[namespace]="data.namespace"
|
||||
[type]="fieldControl.value.type"
|
||||
[field]="fieldControl.value"
|
||||
[parent]="data"
|
||||
[extensions]="data.extensions"
|
||||
></cc-metadata-form>
|
||||
</div>
|
@ -0,0 +1,64 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { ValidationErrors, Validator } from '@angular/forms';
|
||||
import { FormControl } from '@ngneat/reactive-forms';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { FormComponentSuperclass } from '@s-libs/ng-core';
|
||||
import { Field } from '@vality/thrift-ts';
|
||||
import { merge } from 'rxjs';
|
||||
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||
|
||||
import { createValidatedAbstractControlProviders } from '@cc/utils';
|
||||
|
||||
import { MetadataFormData } from '../../types/metadata-form-data';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'cc-union-field',
|
||||
templateUrl: './union-field.component.html',
|
||||
providers: createValidatedAbstractControlProviders(UnionFieldComponent),
|
||||
})
|
||||
export class UnionFieldComponent
|
||||
extends FormComponentSuperclass<{ [N in string]: unknown }>
|
||||
implements OnInit, Validator
|
||||
{
|
||||
@Input() data: MetadataFormData<string, Field[]>;
|
||||
|
||||
fieldControl = new FormControl<Field>();
|
||||
internalControl = new FormControl<unknown>();
|
||||
|
||||
ngOnInit() {
|
||||
this.fieldControl.valueChanges.pipe(untilDestroyed(this)).subscribe(() => {
|
||||
this.internalControl.reset(null, { emitEvent: false });
|
||||
});
|
||||
merge(this.fieldControl.valueChanges, this.internalControl.valueChanges)
|
||||
.pipe(
|
||||
map(() => {
|
||||
const field = this.fieldControl.value;
|
||||
return field ? { [field.name]: this.internalControl.value } : null;
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
untilDestroyed(this)
|
||||
)
|
||||
.subscribe((value) => {
|
||||
this.emitOutgoingValue(value);
|
||||
});
|
||||
}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.fieldControl.invalid || this.internalControl.invalid ? { invalid: true } : null;
|
||||
}
|
||||
|
||||
handleIncomingValue(value: { [N in string]: unknown }) {
|
||||
if (value) {
|
||||
const name = Object.keys(value)[0];
|
||||
this.fieldControl.setValue(
|
||||
this.data.ast.find((f) => f.name === name),
|
||||
{ emitEvent: false }
|
||||
);
|
||||
this.internalControl.setValue(value[name], { emitEvent: false });
|
||||
} else {
|
||||
this.fieldControl.reset(null, { emitEvent: false });
|
||||
this.internalControl.reset(null, { emitEvent: false });
|
||||
}
|
||||
}
|
||||
}
|
2
src/app/shared/components/metadata-form/index.ts
Normal file
2
src/app/shared/components/metadata-form/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './metadata-form.module';
|
||||
export * from './types/metadata-form-data';
|
@ -0,0 +1,30 @@
|
||||
<ng-container [ngSwitch]="data?.typeGroup">
|
||||
<cc-primitive-field
|
||||
*ngSwitchCase="'primitive'"
|
||||
[formControl]="control"
|
||||
[data]="data"
|
||||
></cc-primitive-field>
|
||||
<cc-complex-form
|
||||
*ngSwitchCase="'complex'"
|
||||
[formControl]="control"
|
||||
[data]="data"
|
||||
></cc-complex-form>
|
||||
<ng-container *ngSwitchCase="'object'" [ngSwitch]="data.objectType">
|
||||
<cc-struct-form
|
||||
*ngSwitchCase="'struct'"
|
||||
[formControl]="control"
|
||||
[data]="data"
|
||||
></cc-struct-form>
|
||||
<cc-union-field
|
||||
*ngSwitchCase="'union'"
|
||||
[formControl]="control"
|
||||
[data]="data"
|
||||
></cc-union-field>
|
||||
<cc-enum-field *ngSwitchCase="'enum'" [formControl]="control" [data]="data"></cc-enum-field>
|
||||
<cc-typedef-form
|
||||
*ngSwitchCase="'typedef'"
|
||||
[formControl]="control"
|
||||
[data]="data"
|
||||
></cc-typedef-form>
|
||||
</ng-container>
|
||||
</ng-container>
|
@ -0,0 +1,45 @@
|
||||
import { Component, Input, OnChanges } from '@angular/core';
|
||||
import { ValidationErrors, Validator } from '@angular/forms';
|
||||
import { WrappedFormControlSuperclass } from '@s-libs/ng-core';
|
||||
import { Field, ValueType } from '@vality/thrift-ts';
|
||||
|
||||
import { ThriftAstMetadata } from '@cc/app/api/utils';
|
||||
import { createValidatedAbstractControlProviders } from '@cc/utils';
|
||||
|
||||
import { MetadataFormData, MetadataFormExtension } from './types/metadata-form-data';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-metadata-form',
|
||||
templateUrl: './metadata-form.component.html',
|
||||
providers: createValidatedAbstractControlProviders(MetadataFormComponent),
|
||||
})
|
||||
export class MetadataFormComponent
|
||||
extends WrappedFormControlSuperclass<unknown>
|
||||
implements OnChanges, Validator
|
||||
{
|
||||
@Input() metadata: ThriftAstMetadata[];
|
||||
@Input() namespace: string;
|
||||
@Input() type: ValueType;
|
||||
@Input() field?: Field;
|
||||
@Input() parent?: MetadataFormData;
|
||||
@Input() extensions?: MetadataFormExtension[];
|
||||
|
||||
data: MetadataFormData;
|
||||
|
||||
ngOnChanges() {
|
||||
if (this.metadata && this.namespace && this.type) {
|
||||
this.data = new MetadataFormData(
|
||||
this.metadata,
|
||||
this.namespace,
|
||||
this.type,
|
||||
this.field,
|
||||
this.parent,
|
||||
this.extensions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
validate(): ValidationErrors | null {
|
||||
return this.control.invalid ? { invalid: true } : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FlexModule, GridModule } from '@angular/flex-layout';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
|
||||
import { ThriftPipesModule, ValueTypeTitleModule } from '@cc/app/shared';
|
||||
import { JsonViewerModule } from '@cc/app/shared/components/json-viewer';
|
||||
|
||||
import { ComplexFormComponent } from './components/complex-form/complex-form.component';
|
||||
import { EnumFieldComponent } from './components/enum-field/enum-field.component';
|
||||
import { PrimitiveFieldComponent } from './components/primitive-field/primitive-field.component';
|
||||
import { StructFormComponent } from './components/struct-form/struct-form.component';
|
||||
import { TypedefFormComponent } from './components/typedef-form/typedef-form.component';
|
||||
import { UnionFieldComponent } from './components/union-field/union-field.component';
|
||||
import { MetadataFormComponent } from './metadata-form.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatInputModule,
|
||||
GridModule,
|
||||
ThriftPipesModule,
|
||||
MatSelectModule,
|
||||
MatButtonModule,
|
||||
ReactiveFormsModule,
|
||||
MatAutocompleteModule,
|
||||
MatTooltipModule,
|
||||
MatIconModule,
|
||||
JsonViewerModule,
|
||||
OverlayModule,
|
||||
MatCardModule,
|
||||
MatExpansionModule,
|
||||
FlexModule,
|
||||
ValueTypeTitleModule,
|
||||
],
|
||||
declarations: [
|
||||
MetadataFormComponent,
|
||||
PrimitiveFieldComponent,
|
||||
ComplexFormComponent,
|
||||
StructFormComponent,
|
||||
UnionFieldComponent,
|
||||
TypedefFormComponent,
|
||||
EnumFieldComponent,
|
||||
],
|
||||
exports: [MetadataFormComponent],
|
||||
})
|
||||
export class MetadataFormModule {}
|
@ -0,0 +1,117 @@
|
||||
import { Field, ValueType } from '@vality/thrift-ts';
|
||||
import { JsonAST } from '@vality/thrift-ts/src/thrift-parser';
|
||||
import { Observable, combineLatest, switchMap } from 'rxjs';
|
||||
import { map, pluck, shareReplay } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
isComplexType,
|
||||
isPrimitiveType,
|
||||
parseNamespaceObjectType,
|
||||
parseNamespaceType,
|
||||
StructureType,
|
||||
ThriftAstMetadata,
|
||||
} from '@cc/app/api/utils';
|
||||
|
||||
export interface MetadataFormExtensionResult {
|
||||
options?: {
|
||||
value: unknown;
|
||||
label?: string;
|
||||
details?: string | object;
|
||||
}[];
|
||||
generate?: () => Observable<unknown>;
|
||||
}
|
||||
|
||||
export type MetadataFormExtension = {
|
||||
determinant: (data: MetadataFormData) => Observable<boolean>;
|
||||
extension: (data: MetadataFormData) => Observable<MetadataFormExtensionResult>;
|
||||
};
|
||||
|
||||
export enum TypeGroup {
|
||||
Complex = 'complex',
|
||||
Primitive = 'primitive',
|
||||
Object = 'object',
|
||||
}
|
||||
|
||||
export function getAliases(data: MetadataFormData) {
|
||||
const path: MetadataFormData[] = data.parent ? [data.parent] : [];
|
||||
while (path[path.length - 1]?.objectType === 'typedef') {
|
||||
path.push(path[path.length - 1].parent);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function getByType(data: MetadataFormData, type: string, namespace: string) {
|
||||
return [data, ...getAliases(data)].find((d) => d.type === type && d.namespace === namespace);
|
||||
}
|
||||
|
||||
type ObjectAst = JsonAST[keyof JsonAST][keyof JsonAST[keyof JsonAST]];
|
||||
|
||||
export class MetadataFormData<T extends ValueType = ValueType, M extends ObjectAst = ObjectAst> {
|
||||
typeGroup: TypeGroup;
|
||||
|
||||
namespace: string;
|
||||
type: T;
|
||||
|
||||
objectType?: StructureType;
|
||||
ast?: M;
|
||||
|
||||
get typedefData() {
|
||||
let data: MetadataFormData = this as MetadataFormData;
|
||||
while (data.parent?.objectType === 'typedef') {
|
||||
data = data.parent;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first one identified is used
|
||||
*/
|
||||
extensionResult$: Observable<MetadataFormExtensionResult>;
|
||||
|
||||
constructor(
|
||||
public metadata: ThriftAstMetadata[],
|
||||
namespace: string,
|
||||
type: T,
|
||||
public field?: Field,
|
||||
public parent?: MetadataFormData,
|
||||
public extensions?: MetadataFormExtension[]
|
||||
) {
|
||||
this.setNamespaceType(namespace, type);
|
||||
this.setTypeGroup();
|
||||
if (this.typeGroup === TypeGroup.Object) this.setNamespaceObjectType();
|
||||
}
|
||||
|
||||
private setNamespaceType(namespace: string, type: T) {
|
||||
const namespaceType = parseNamespaceType<T>(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) {
|
||||
this.typeGroup = isComplexType(type)
|
||||
? TypeGroup.Complex
|
||||
: isPrimitiveType(this.type)
|
||||
? TypeGroup.Primitive
|
||||
: TypeGroup.Object;
|
||||
}
|
||||
|
||||
private setNamespaceObjectType() {
|
||||
const namespaceObjectType = parseNamespaceObjectType(
|
||||
this.metadata,
|
||||
this.namespace,
|
||||
this.type as string
|
||||
);
|
||||
this.objectType = namespaceObjectType.objectType;
|
||||
this.ast = (namespaceObjectType.namespaceMetadata.ast[this.objectType] as unknown)[
|
||||
this.type
|
||||
] as M;
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export * from './thrift';
|
||||
export * from './common';
|
||||
export * from './api-model-types';
|
||||
export * from './value-type-title/value-type-title.module';
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { ValueTypeTitlePipe } from './value-type-title.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ValueTypeTitlePipe],
|
||||
imports: [CommonModule],
|
||||
exports: [ValueTypeTitlePipe],
|
||||
})
|
||||
export class ValueTypeTitleModule {}
|
@ -0,0 +1,26 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { ValueType } from '@vality/thrift-ts';
|
||||
import lowerCase from 'lodash-es/lowerCase';
|
||||
|
||||
import { isComplexType } from '@cc/app/api/utils';
|
||||
|
||||
@Pipe({
|
||||
name: 'valueTypeTitle',
|
||||
})
|
||||
export class ValueTypeTitlePipe implements PipeTransform {
|
||||
transform(valueType: ValueType): string {
|
||||
return this.getTypeName(valueType);
|
||||
}
|
||||
|
||||
private getTypeName(valueType: ValueType): string {
|
||||
if (isComplexType(valueType)) {
|
||||
if (valueType.name === 'map') {
|
||||
return `${valueType.name}: ${this.getTypeName(
|
||||
valueType.keyType
|
||||
)} - ${this.getTypeName(valueType.valueType)}`;
|
||||
}
|
||||
return `${valueType.name}: ${this.getTypeName(valueType.valueType)}`;
|
||||
}
|
||||
return lowerCase(valueType);
|
||||
}
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
<div fxLayout="column" fxLayoutGap="8px">
|
||||
<div class="cc-details-item-title mat-caption">{{ title }}</div>
|
||||
<div class="mat-body-1">
|
||||
<div #content class="cc-details-item-content"><ng-content></ng-content></div>
|
||||
<div class="cc-details-item-content-empty" *ngIf="!content.innerHTML.length">-</div>
|
||||
<div class="mat-body-1 cc-details-item-content">
|
||||
<div #content>
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div class="cc-details-item-content-empty" *ngIf="isEmpty$ | async">
|
||||
<mat-icon inline>hide_source</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,11 +1,41 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'cc-details-item',
|
||||
templateUrl: 'details-item.component.html',
|
||||
styleUrls: ['details-item.component.scss'],
|
||||
})
|
||||
export class DetailsItemComponent {
|
||||
@Input()
|
||||
title: string;
|
||||
export class DetailsItemComponent implements AfterViewInit {
|
||||
@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())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import '../timeline-item.scss';
|
||||
|
||||
$size: 36px;
|
||||
$size: 48px;
|
||||
$line-size: 2px;
|
||||
|
||||
.cc-timeline-item-badge {
|
||||
|
@ -1,5 +1,6 @@
|
||||
@import '../timeline-item.scss';
|
||||
|
||||
.cc-timeline-item-content {
|
||||
display: block;
|
||||
padding-top: $content-padding;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
<div class="cc-timeline-item-title">
|
||||
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center"><ng-content></ng-content></div>
|
||||
<div fxFlex fxLayout="row" fxLayoutAlign="space-between center" class="cc-subheading-2">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import './timeline-item.scss';
|
||||
|
||||
.cc-timeline-item {
|
||||
padding-bottom: 32px;
|
||||
padding-left: $size + $content-padding + 6;
|
||||
padding-bottom: 24px;
|
||||
padding-left: $size + 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
$size: 36px;
|
||||
$icons-size: 18px;
|
||||
$size: 48px;
|
||||
$icons-size: 24px;
|
||||
$line-size: 2px;
|
||||
$content-padding: 8px;
|
||||
|
@ -1,4 +1,4 @@
|
||||
$size: 36px;
|
||||
$size: 48px;
|
||||
$line-size: 2px;
|
||||
|
||||
.cc-timeline {
|
||||
|
Loading…
Reference in New Issue
Block a user