TD-371: Add datetime to metadata form (#125)

This commit is contained in:
Rinat Arsaev 2022-08-16 13:28:52 +03:00 committed by GitHub
parent b0d9288569
commit 516f21ab0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 232 additions and 127 deletions

View File

@ -26,7 +26,6 @@ import { DomainModule } from './domain';
import icons from './icons.json';
import { NotFoundModule } from './not-found';
import { RepairingModule } from './repairing/repairing.module';
import { ClaimModule } from './sections/claim';
import { DomainConfigModule } from './sections/domain-config';
import { OperationsModule } from './sections/operations/operations.module';
import { PaymentAdjustmentModule } from './sections/payment-adjustment/payment-adjustment.module';
@ -72,7 +71,6 @@ moment.locale('en-GB');
DomainConfigModule,
KeycloakTokenInfoModule,
PayoutsModule,
ClaimModule,
SectionsModule,
// It is important that NotFoundModule module should be last
NotFoundModule,
@ -81,7 +79,7 @@ moment.locale('en-GB');
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } },
{ provide: MAT_DATE_FORMATS, useValue: DEFAULT_MAT_DATE_FORMATS },
{ provide: DateAdapter, useClass: MomentUtcDateAdapter, deps: [MAT_DATE_LOCALE] },
{ provide: MAT_DATE_LOCALE, useValue: 'en' },
{ provide: MAT_DATE_LOCALE, useValue: 'ru' },
{ provide: LOCALE_ID, useValue: 'en' },
{ provide: SEARCH_LIMIT, useValue: DEFAULT_SEARCH_LIMIT },
{ provide: SMALL_SEARCH_LIMIT, useValue: DEFAULT_SMALL_SEARCH_LIMIT },

View File

@ -9,7 +9,7 @@ import { ClaimComponent } from './claim.component';
imports: [
RouterModule.forChild([
{
path: 'party/:partyID/claim/:claimID',
path: '',
component: ClaimComponent,
canActivate: [AppAuthGuardService],
data: {

View File

@ -1,20 +1,13 @@
<div *ngIf="(refunds$ | async)?.length; else empty" fxLayout="column" fxLayoutGap="24px">
<cc-refunds-table [refunds]="refunds$ | async"></cc-refunds-table>
<button
*ngIf="hasMore$ | async"
[disabled]="doAction$ | async"
fxFlex="100"
mat-button
(click)="fetchMore()"
>
{{ (doAction$ | async) ? 'LOADING...' : 'SHOW MORE' }}
</button>
<div *ngIf="isLoading$ | async; else content" fxLayout fxLayoutAlign="center center">
<mat-spinner diameter="64"></mat-spinner>
</div>
<ng-template #empty>
<div *ngIf="isLoading$ | async; else emptyResult" fxLayout fxLayoutAlign="center center">
<mat-spinner diameter="64"></mat-spinner>
<ng-template #content>
<div fxLayout="column" fxLayoutGap="24px">
<cc-refunds-table [refunds]="refunds$ | async"></cc-refunds-table>
<cc-show-more-button
*ngIf="hasMore$ | async"
[inProgress]="doAction$ | async"
(fetchMore)="fetchMore()"
></cc-show-more-button>
</div>
<ng-template #emptyResult>
<cc-empty-search-result unwrapped></cc-empty-search-result>
</ng-template>
</ng-template>

View File

@ -5,6 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { EmptySearchResultModule } from '@cc/components/empty-search-result';
import { TableModule } from '@cc/components/table';
import { FetchRefundsService } from './fetch-refunds.service';
import { PaymentRefundsComponent } from './payment-refunds.component';
@ -20,6 +21,7 @@ import { RefundsTableModule } from './refunds-table';
MatProgressSpinnerModule,
EmptySearchResultModule,
MatButtonModule,
TableModule,
],
exports: [PaymentRefundsComponent],
})

View File

@ -27,6 +27,7 @@
</td>
</ng-container>
<cc-no-data-row></cc-no-data-row>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table>

View File

@ -6,6 +6,7 @@ import { MatTableModule } from '@angular/material/table';
import { StatusModule } from '@cc/app/shared/components/status';
import { CommonPipesModule, ThriftPipesModule, AmountCurrencyPipe } from '@cc/app/shared/pipes';
import { TableModule } from '@cc/components/table';
import { RefundsTableComponent } from './refunds-table.component';
@ -19,6 +20,7 @@ import { RefundsTableComponent } from './refunds-table.component';
ThriftPipesModule,
CommonPipesModule,
AmountCurrencyPipe,
TableModule,
],
declarations: [RefundsTableComponent],
exports: [RefundsTableComponent],

View File

@ -7,8 +7,21 @@ const ROUTES: Routes = [
loadChildren: () => import('./party/party.module').then((m) => m.PartyModule),
},
{
path: 'party/:partyID/invoice/:invoiceID/payment/:paymentID',
loadChildren: () => import('./payment-details').then((m) => m.PaymentDetailsModule),
path: 'party',
loadChildren: () => import('./party/party.module').then((m) => m.PartyModule),
},
{
path: 'party/:partyID',
children: [
{
path: 'claim/:claimID',
loadChildren: () => import('./claim').then((m) => m.ClaimModule),
},
{
path: 'invoice/:invoiceID/payment/:paymentID',
loadChildren: () => import('./payment-details').then((m) => m.PaymentDetailsModule),
},
],
},
{
path: 'withdrawals',

View File

@ -0,0 +1,19 @@
<div fxLayout fxLayoutGap="8px">
<mat-form-field fxFlex>
<mat-label>
{{ label }}
</mat-label>
<mat-hint>{{ hint }}</mat-hint>
<input
[matDatepicker]="picker"
[required]="required"
matInput
(dateInput)="dateChanged($event)"
/>
<mat-datepicker-toggle [for]="picker" matSuffix></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-form-field *ngIf="datetime" style="width: 100px">
<input [value]="time" matInput type="time" (input)="timeChanged($event)" />
</mat-form-field>
</div>

View File

@ -0,0 +1,53 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { ValidationErrors } from '@angular/forms';
import { MatDatepickerModule, MatDatepickerInputEvent } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { FormComponentSuperclass } from '@s-libs/ng-core';
import { coerceBoolean } from 'coerce-property';
import moment from 'moment';
import { Moment } from 'moment/moment';
import { createControlProviders } from '@cc/utils';
@Component({
selector: 'cc-datetime',
standalone: true,
imports: [CommonModule, MatFormFieldModule, MatInputModule, MatDatepickerModule, FlexModule],
templateUrl: './datetime.component.html',
providers: createControlProviders(DatetimeComponent),
})
export class DatetimeComponent extends FormComponentSuperclass<string> {
@Input() label: string;
@Input() @coerceBoolean required = false;
@Input() hint?: string;
datetime: Moment;
get time() {
return this.datetime ? this.datetime.format('HH:mm') : '';
}
handleIncomingValue(value: string) {
this.datetime = value ? moment(value) : null;
}
timeChanged(event: Event) {
const [hours, minutes] = (event.target as HTMLInputElement).value.split(':');
this.datetime = this.datetime
.clone()
.set({ minutes: Number(minutes), hours: Number(hours) });
this.emitOutgoingValue(this.datetime.toISOString());
}
dateChanged(date: MatDatepickerInputEvent<Moment>) {
this.datetime = date.target.value;
this.emitOutgoingValue(this.datetime.toISOString());
}
validate(): ValidationErrors | null {
return !this.datetime || this.datetime.isValid() ? null : { invalidDatetime: true };
}
}

View File

@ -1,7 +1,6 @@
<div gdColumns="1fr" gdGap="16px">
<span *ngIf="hasLabel" class="cc-body-1">
<cc-field-label [field]="data.field" [type]="data.type"></cc-field-label>
({{ data.type.name | titlecase }})
{{ data.type | fieldLabel: data.field }} ({{ data.type.name | titlecase }})
</span>
<mat-accordion [ngStyle]="{ 'padding-left': hasLabel && '16px' }">
<mat-expansion-panel

View File

@ -1,7 +1,5 @@
<mat-form-field style="width: 100%">
<mat-label>
<cc-field-label [field]="data.field" [type]="data.type"></cc-field-label>
</mat-label>
<mat-label>{{ data.type | fieldLabel: data.field }}</mat-label>
<mat-select [formControl]="control" [required]="data.isRequired">
<mat-option
*ngFor="let item of data.ast.items; let idx = index"

View File

@ -1 +0,0 @@
{{ field ? (field.name | keyTitle | titlecase) : (type | valueTypeTitle | titlecase) }}

View File

@ -1,11 +0,0 @@
import { Component, Input } from '@angular/core';
import { Field, ValueType } from '@vality/thrift-ts';
@Component({
selector: 'cc-field-label',
templateUrl: './field-label.component.html',
})
export class FieldLabelComponent {
@Input() type: ValueType;
@Input() field?: Field;
}

View File

@ -1,11 +1,7 @@
<div gdColumns="1fr" gdGap="16px">
<ng-container *ngIf="data.type === 'bool'; else input">
<div gdColumns="1fr" gdGap="16px">
<cc-field-label
[field]="data.field"
[type]="data.type"
class="cc-body-1"
></cc-field-label>
<span class="cc-body-1">{{ data.type | fieldLabel: data.field }}</span>
<div fxLayoutAlign=" center" fxLayoutGap="4px">
<mat-radio-group [formControl]="control" fxFlex gdColumns="1fr 1fr" gdGap="8px">
<mat-radio-button [value]="false">False</mat-radio-button>
@ -28,65 +24,90 @@
</ng-container>
<ng-template #input>
<div fxLayoutGap="4px">
<mat-form-field fxFlex>
<mat-label>
<cc-field-label
*ngIf="!(data.extensionResult$ | async)?.label; else extensionLabel"
[field]="data.field"
[type]="data.type"
></cc-field-label>
<ng-template #extensionLabel>{{
(data.extensionResult$ | async).label
}}</ng-template>
</mat-label>
<input
#trigger="matAutocompleteTrigger"
<ng-container *ngIf="(data.extensionResult$ | async)?.type === 'datetime'; else input">
<cc-datetime
[formControl]="control"
[matAutocomplete]="auto"
[ngClass]="{ 'cc-code': (data.extensionResult$ | async)?.isIdentifier }"
[required]="data.isRequired"
[type]="inputType"
matInput
/>
<div fxLayoutGap="4px" matSuffix>
<button
*ngIf="!data.isRequired && control.value"
mat-icon-button
(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-hint>{{ aliases }}</mat-hint>
<mat-autocomplete #auto="matAutocomplete">
<ng-container *ngIf="data.extensionResult$ | async as extensionResult">
<mat-option
*ngFor="let option of filteredOptions$ | async"
[value]="option.value"
[hint]="aliases"
[label]="
(data.extensionResult$ | async)?.label ??
(data.type | fieldLabel: data.field)
"
fxFlex
></cc-datetime>
<button
*ngIf="!data.isRequired && control.value"
mat-icon-button
(click)="clear($event)"
>
<mat-icon>clear</mat-icon>
</button>
<button *ngIf="generate$ | async" mat-icon-button (click)="generate($event)">
<mat-icon>loop</mat-icon>
</button>
</ng-container>
<ng-template #input>
<mat-form-field fxFlex>
<mat-label>
<ng-container
*ngIf="!(data.extensionResult$ | async)?.label; else extensionLabel"
>{{ data.type | fieldLabel: data.field }}</ng-container
>
<div fxLayout="row" fxLayoutAlign=" center" fxLayoutGap="8px">
<div [ngClass]="{ 'cc-code': extensionResult.isIdentifier }">
{{ option.value }}
<ng-template #extensionLabel>{{
(data.extensionResult$ | async).label
}}</ng-template>
</mat-label>
<mat-hint>{{ aliases }}</mat-hint>
<input
#trigger="matAutocompleteTrigger"
[formControl]="control"
[matAutocomplete]="auto"
[ngClass]="{ 'cc-code': (data.extensionResult$ | async)?.isIdentifier }"
[required]="data.isRequired"
[type]="inputType"
matInput
/>
<div fxLayoutGap="4px" matSuffix>
<button
*ngIf="!data.isRequired && control.value"
mat-icon-button
(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 filteredOptions$ | async"
[value]="option.value"
>
<div fxLayout="row" fxLayoutAlign=" center" fxLayoutGap="8px">
<div [ngClass]="{ 'cc-code': extensionResult.isIdentifier }">
{{ option.value }}
</div>
<cc-label
[color]="option.color"
[label]="option.label"
></cc-label>
</div>
<cc-label [color]="option.color" [label]="option.label"></cc-label>
</div>
</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>
</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>
</ng-template>
</div>
</ng-template>
<ng-container *ngIf="selected$ | async as selected">

View File

@ -4,11 +4,7 @@
<ng-container [ngTemplateOutlet]="label"></ng-container>
</mat-checkbox>
<ng-template #label>
<cc-field-label
[field]="data.field"
[type]="data.type"
class="cc-body-1"
></cc-field-label>
<span class="cc-body-1">{{ data.type | fieldLabel: data.field }}</span>
</ng-template>
</ng-container>
<ng-container *ngIf="labelControl.value">

View File

@ -1,16 +1,14 @@
<div gdColumns="1fr" gdGap="16px">
<mat-form-field style="width: 100%">
<mat-label>
<cc-field-label [field]="data.field" [type]="data.type"> </cc-field-label>
</mat-label>
<mat-label>{{ data.type | fieldLabel: data.field }}</mat-label>
<mat-select
[formControl]="fieldControl"
[required]="data.isRequired"
(ngModelChange)="cleanInternal()"
>
<mat-option *ngFor="let field of data.ast" [value]="field">
<cc-field-label [field]="field" [type]="field.type"></cc-field-label>
</mat-option>
<mat-option *ngFor="let field of data.ast" [value]="field">{{
field.type | fieldLabel: field
}}</mat-option>
</mat-select>
<button
*ngIf="fieldControl.value && data.isRequired"

View File

@ -8,6 +8,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
@ -15,19 +16,20 @@ import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatTooltipModule } from '@angular/material/tooltip';
import { DatetimeComponent } from '@cc/app/shared/components/datetime/datetime.component';
import { JsonViewerModule } from '@cc/app/shared/components/json-viewer';
import { ThriftPipesModule } from '@cc/app/shared/pipes/thrift';
import { ValueTypeTitleModule } from '@cc/app/shared/pipes/value-type-title';
import { ComplexFormComponent } from './components/complex-form/complex-form.component';
import { EnumFieldComponent } from './components/enum-field/enum-field.component';
import { FieldLabelComponent } from './components/field-label/field-label.component';
import { LabelComponent } from './components/label/label.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';
import { FieldLabelPipe } from './pipes/field-label.pipe';
@NgModule({
imports: [
@ -50,6 +52,8 @@ import { MetadataFormComponent } from './metadata-form.component';
MatCheckboxModule,
MatChipsModule,
MatRadioModule,
MatDatepickerModule,
DatetimeComponent,
],
declarations: [
MetadataFormComponent,
@ -59,8 +63,8 @@ import { MetadataFormComponent } from './metadata-form.component';
UnionFieldComponent,
TypedefFormComponent,
EnumFieldComponent,
FieldLabelComponent,
LabelComponent,
FieldLabelPipe,
],
exports: [MetadataFormComponent],
})

View File

@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
import { ValueType, Field } from '@vality/thrift-ts';
import startCase from 'lodash-es/startCase';
import { getValueTypeTitle } from '@cc/app/shared';
@Pipe({
name: 'fieldLabel',
})
export class FieldLabelPipe implements PipeTransform {
transform(type: ValueType, field?: Field): string {
return type ? startCase((field ? field.name : getValueTypeTitle(type)).toLowerCase()) : '';
}
}

View File

@ -3,21 +3,22 @@ import { Observable } from 'rxjs';
import { MetadataFormData } from './metadata-form-data';
export type MetadataFormExtension = {
determinant: (data: MetadataFormData) => Observable<boolean>;
extension: (data: MetadataFormData) => Observable<MetadataFormExtensionResult>;
};
export interface MetadataFormExtensionResult {
options?: MetadataFormExtensionOption[];
generate?: () => Observable<unknown>;
isIdentifier?: boolean;
label?: string;
type?: 'datetime';
}
export interface MetadataFormExtensionOption {
value: unknown;
label?: string;
details?: string | object;
color?: ThemePalette;
}
export interface MetadataFormExtensionResult {
options?: MetadataFormExtensionOption[];
generate?: () => Observable<unknown>;
isIdentifier?: boolean;
label?: string;
}
export type MetadataFormExtension = {
determinant: (data: MetadataFormData) => Observable<boolean>;
extension: (data: MetadataFormData) => Observable<MetadataFormExtensionResult>;
};

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { DomainObject } 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';
import uuid from 'uuid';
@ -29,12 +30,16 @@ export class DomainMetadataFormExtensionsService {
(m) => m.default as never as ThriftAstMetadata[]
)
).pipe(
map((metadata) => [
map((metadata): MetadataFormExtension[] => [
...this.createDomainObjectsOptions(metadata),
{
determinant: (data) => of(isTypeWithAliases(data, 'ID', 'base')),
extension: () => of({ generate: () => of(uuid()), isIdentifier: true }),
},
{
determinant: (data) => of(isTypeWithAliases(data, 'Timestamp', 'base')),
extension: () => of({ type: 'datetime', generate: () => of(moment()) }),
},
]),
shareReplay(1)
);