IMP-56: New repairing table and provider autocomplete (#210)

This commit is contained in:
Rinat Arsaev 2023-04-07 15:07:31 +04:00 committed by GitHub
parent ea4a714499
commit fafe19bdfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 511 additions and 233 deletions

140
package-lock.json generated
View File

@ -22,6 +22,7 @@
"@angular/platform-browser-dynamic": "15.0.3",
"@angular/platform-server": "15.0.3",
"@angular/router": "15.0.3",
"@ng-matero/extensions": "15.3.0",
"@ngneat/input-mask": "6.0.0",
"@ngneat/until-destroy": "9.2.2",
"@s-libs/js-core": "15.0.0",
@ -2818,6 +2819,14 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz",
"integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ==",
"engines": {
"node": ">=10"
}
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"license": "MIT",
@ -3819,6 +3828,46 @@
"tslib": "^2.1.0"
}
},
"node_modules/@ng-matero/extensions": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@ng-matero/extensions/-/extensions-15.3.0.tgz",
"integrity": "sha512-NEPK9ba437d+eDRtj0wSZFUUTCpJlWuJr2ZpbTBwQ/mwN6eyprASM9dq4ydqEcGE+uefxZrSZrVp+eaztNjHjA==",
"dependencies": {
"@ng-select/ng-select": "^10.0.0",
"ngx-color": "^8.0.0",
"photoviewer": "^3.6.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@angular/animations": ">=15.0.0",
"@angular/cdk": ">=15.0.0",
"@angular/common": ">=15.0.0",
"@angular/core": ">=15.0.0",
"@angular/material": ">=15.0.0"
}
},
"node_modules/@ng-matero/extensions/node_modules/@ng-select/ng-select": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-10.0.4.tgz",
"integrity": "sha512-Vc/JIgcFkSgf47cX7+pQQo9HYhDktfqrY7o/ZPGMvu63P7E9d1MibVipqmcLbgms6Ac9lu621CDZPGHdxag7hA==",
"dependencies": {
"tslib": "^2.3.1"
},
"engines": {
"node": ">= 12.20.0",
"npm": ">= 6.0.0"
},
"peerDependencies": {
"@angular/common": "<16.0.0",
"@angular/core": "<16.0.0",
"@angular/forms": "<16.0.0"
}
},
"node_modules/@ng-matero/extensions/node_modules/tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
},
"node_modules/@ngneat/input-mask": {
"version": "6.0.0",
"license": "MIT",
@ -9289,6 +9338,11 @@
"version": "2.1.6",
"license": "BSD-2-Clause"
},
"node_modules/domq.js": {
"version": "0.6.7",
"resolved": "https://registry.npmjs.org/domq.js/-/domq.js-0.6.7.tgz",
"integrity": "sha512-WwRGORo/eYGf7v7YXZ3M6x/PEoxCsP3D0my7pnAVwtbfsKRvW6qioSdlLsy1MFzfwN1TM9oO9QJcvkE8ERYmlg=="
},
"node_modules/domutils": {
"version": "2.8.0",
"dev": true,
@ -13596,6 +13650,11 @@
"node": ">=0.10.0"
}
},
"node_modules/material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"node_modules/md5.js": {
"version": "1.3.5",
"license": "MIT",
@ -14357,6 +14416,20 @@
"rxjs": ">= 6.0.0"
}
},
"node_modules/ngx-color": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-8.0.3.tgz",
"integrity": "sha512-tuLP+uIoDEu2m0bh711kb2P1M1bh/oIrOn8mJd9mb8xGL2v+OcokcxPmVvWRn0avMG1lXL53CjSlWXGkdV4CDA==",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"material-colors": "^1.2.6",
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">=14.0.0-0",
"@angular/core": ">=14.0.0-0"
}
},
"node_modules/ngx-mat-select-search": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/ngx-mat-select-search/-/ngx-mat-select-search-7.0.1.tgz",
@ -15657,6 +15730,14 @@
"node": ">=0.12"
}
},
"node_modules/photoviewer": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/photoviewer/-/photoviewer-3.6.6.tgz",
"integrity": "sha512-TYuxoEdlVkIngVnoCEO+CWjeTO/F4TOPDv9ic4zbVU8ZXMiDggUzMj7jv50Kl0n1Yks72hleujPjAfGmkl7n9w==",
"dependencies": {
"domq.js": "^0.6.7"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"license": "ISC"
@ -21195,6 +21276,11 @@
}
}
},
"@ctrl/tinycolor": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz",
"integrity": "sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ=="
},
"@discoveryjs/json-ext": {
"version": "0.5.7"
},
@ -22032,6 +22118,32 @@
"tslib": "^2.1.0"
}
},
"@ng-matero/extensions": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@ng-matero/extensions/-/extensions-15.3.0.tgz",
"integrity": "sha512-NEPK9ba437d+eDRtj0wSZFUUTCpJlWuJr2ZpbTBwQ/mwN6eyprASM9dq4ydqEcGE+uefxZrSZrVp+eaztNjHjA==",
"requires": {
"@ng-select/ng-select": "^10.0.0",
"ngx-color": "^8.0.0",
"photoviewer": "^3.6.0",
"tslib": "^2.4.0"
},
"dependencies": {
"@ng-select/ng-select": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-10.0.4.tgz",
"integrity": "sha512-Vc/JIgcFkSgf47cX7+pQQo9HYhDktfqrY7o/ZPGMvu63P7E9d1MibVipqmcLbgms6Ac9lu621CDZPGHdxag7hA==",
"requires": {
"tslib": "^2.3.1"
}
},
"tslib": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
}
}
},
"@ngneat/input-mask": {
"version": "6.0.0",
"requires": {
@ -25726,6 +25838,11 @@
"domino": {
"version": "2.1.6"
},
"domq.js": {
"version": "0.6.7",
"resolved": "https://registry.npmjs.org/domq.js/-/domq.js-0.6.7.tgz",
"integrity": "sha512-WwRGORo/eYGf7v7YXZ3M6x/PEoxCsP3D0my7pnAVwtbfsKRvW6qioSdlLsy1MFzfwN1TM9oO9QJcvkE8ERYmlg=="
},
"domutils": {
"version": "2.8.0",
"dev": true,
@ -28469,6 +28586,11 @@
"object-visit": "^1.0.0"
}
},
"material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"md5.js": {
"version": "1.3.5",
"requires": {
@ -28994,6 +29116,16 @@
"webpack-merge": "^5.0.0"
}
},
"ngx-color": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/ngx-color/-/ngx-color-8.0.3.tgz",
"integrity": "sha512-tuLP+uIoDEu2m0bh711kb2P1M1bh/oIrOn8mJd9mb8xGL2v+OcokcxPmVvWRn0avMG1lXL53CjSlWXGkdV4CDA==",
"requires": {
"@ctrl/tinycolor": "^3.4.1",
"material-colors": "^1.2.6",
"tslib": "^2.3.0"
}
},
"ngx-mat-select-search": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/ngx-mat-select-search/-/ngx-mat-select-search-7.0.1.tgz",
@ -29850,6 +29982,14 @@
"sha.js": "^2.4.8"
}
},
"photoviewer": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/photoviewer/-/photoviewer-3.6.6.tgz",
"integrity": "sha512-TYuxoEdlVkIngVnoCEO+CWjeTO/F4TOPDv9ic4zbVU8ZXMiDggUzMj7jv50Kl0n1Yks72hleujPjAfGmkl7n9w==",
"requires": {
"domq.js": "^0.6.7"
}
},
"picocolors": {
"version": "1.0.0"
},

View File

@ -12,7 +12,7 @@
"build-libs": "ng build ng-core",
"build": "npm run build-libs && npm run build-app",
"test": "ng test",
"lint": "eslint \"src/**/*.{ts,js,html}\" --max-warnings 552",
"lint": "eslint \"src/**/*.{ts,js,html}\" --max-warnings 420",
"lint-fix": "npm run lint -- --fix",
"lint-errors": "npm run lint -- --quiet",
"lint-libs": "eslint \"projects/**/*.{ts,js,html}\" --max-warnings 0",
@ -36,6 +36,7 @@
"@angular/platform-browser-dynamic": "15.0.3",
"@angular/platform-server": "15.0.3",
"@angular/router": "15.0.3",
"@ng-matero/extensions": "15.3.0",
"@ngneat/input-mask": "6.0.0",
"@ngneat/until-destroy": "9.2.2",
"@s-libs/js-core": "15.0.0",

View File

@ -13,10 +13,10 @@
<input formControlName="ns" matInput />
</mat-form-field>
<cc-date-range formControlName="timespan"></cc-date-range>
<mat-form-field>
<mat-label>Provider ID</mat-label>
<input formControlName="provider_id" matInput />
</mat-form-field>
<cc-domain-object-field
formControlName="provider_id"
name="provider"
></cc-domain-object-field>
<mat-form-field>
<mat-label>Status</mat-label>
<mat-select formControlName="status">
@ -34,16 +34,24 @@
<input formControlName="error_message" matInput />
</mat-form-field>
</mat-card-content>
<mat-card-footer *ngIf="inProgress$ | async">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-card-footer>
</mat-card>
<ng-container *ngIf="!(inProgress$ | async) || (machines$ | async)">
<cc-actions>
<button mat-button (click)="update()">UPDATE</button>
<cc-simple-table
[cellTemplate]="cellTemplate"
[columns]="columns$ | async"
[data]="machines$ | async"
[hasMore]="hasMore$ | async"
[loading]="(inProgress$ | async) && !!(columns$ | async)"
[trackBy]="trackById"
rowSelectable
(fetchMore)="fetchMore()"
(rowSelectionChange)="selected$.next($event)"
(size)="update($event)"
(update)="update($event.size)"
>
<cc-simple-table-actions>
<button
[disabled]="!selection?.selected?.length"
[disabled]="!(selected$ | async)?.length"
color="primary"
mat-button
(click)="repairByScenario()"
@ -51,82 +59,17 @@
REPAIR BY SCENARIO
</button>
<button
[disabled]="!selection?.selected?.length"
[disabled]="!(selected$ | async)?.length"
color="primary"
mat-button
(click)="repair()"
>
SIMPLE REPAIR
</button>
</cc-actions>
<cc-empty-search-result *ngIf="!(machines$ | async)?.length"></cc-empty-search-result>
<mat-card *ngIf="(machines$ | async)?.length" fxLayout="column" fxLayoutGap="18px">
<table [dataSource]="machines$ | async" mat-table>
<cc-select-column
[dataSource]="machines$ | async"
(changed)="selection = $event"
></cc-select-column>
<ng-container [matColumnDef]="cols.def.id">
<th *matHeaderCellDef mat-header-cell>ID</th>
<td *matCellDef="let i" mat-cell>{{ i.id }}</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.namespace">
<th *matHeaderCellDef mat-header-cell>Namespace</th>
<td *matCellDef="let i" mat-cell>{{ i.ns }}</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.createdAt">
<th *matHeaderCellDef mat-header-cell>Created at</th>
<td *matCellDef="let i" mat-cell>
{{ i.created_at | date : 'dd.MM.yyyy HH:mm:ss' }}
</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.status">
<th *matHeaderCellDef mat-header-cell matTooltip="Hover to view details">
<span matBadge="" matBadgeOverlap="false" matBadgeSize="small">
Status
</span>
</th>
<td
*matCellDef="let i"
[matTooltip]="i.error_message"
mat-cell
matTooltipPosition="left"
>
{{ i.status | enumKey : status }}
</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.provider">
<th *matHeaderCellDef mat-header-cell>Provider</th>
<td *matCellDef="let i" mat-cell>
{{ i.provider_id }}
</td>
</ng-container>
<ng-container [matColumnDef]="cols.def.history">
<th *matHeaderCellDef mat-header-cell matTooltip="Hover to view details">
<span matBadge="" matBadgeOverlap="false" matBadgeSize="small"
>History</span
>
</th>
<td
*matCellDef="let i"
[matTooltip]="i.history?.length ? (i.history | json) : ''"
mat-cell
matTooltipPosition="left"
>
{{ i.history?.length || '' }}
</td>
</ng-container>
<tr *matHeaderRowDef="cols.list" mat-header-row></tr>
<tr *matRowDef="let row; columns: cols.list" mat-row></tr>
</table>
<cc-show-more-button
*ngIf="hasMore$ | async"
[inProgress]="inProgress$ | async"
(fetchMore)="fetchMore()"
></cc-show-more-button>
</mat-card>
</ng-container>
</cc-simple-table-actions>
<cc-simple-table-tooltip-cell-template
(template)="cellTemplate.status = cellTemplate.history = $event"
></cc-simple-table-tooltip-cell-template>
</cc-simple-table>
</div>
</div>

View File

@ -1,18 +1,26 @@
import { SelectionModel } from '@angular/cdk/collections';
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, TemplateRef } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { DateRange } from '@angular/material/datepicker';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BaseDialogResponseStatus, BaseDialogService, clean, splitIds } from '@vality/ng-core';
import { repairer } from '@vality/repairer-proto';
import { Namespace, ProviderID, RepairStatus, Machine } from '@vality/repairer-proto/repairer';
import isNil from 'lodash-es/isNil';
import { Moment } from 'moment';
import { filter, map, switchMap } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { filter, map, switchMap, shareReplay } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/deprecated-damsel';
import { NotificationErrorService } from '@cc/app/shared/services/notification-error';
import { ConfirmActionDialogComponent } from '@cc/components/confirm-action-dialog';
import {
createGridColumns,
createDatetimeFormattedColumn,
createDescriptionFormattedColumn,
} from '@cc/components/simple-table';
import { createTooltipTemplateGridColumn } from '@cc/components/simple-table/components/simple-table-tooltip-cell-template.component';
import { getEnumKey } from '@cc/utils';
import { ConfirmActionDialogComponent } from '../../../components/confirm-action-dialog';
import { Columns, SELECT_COLUMN_NAME } from '../../../components/table';
import { RepairManagementService } from '../../api/repairer';
import { QueryParamsService } from '../../shared/services';
import { NotificationService } from '../../shared/services/notification';
@ -55,17 +63,41 @@ export class RepairingComponent implements OnInit {
error_message: null,
...this.qp.params,
});
selection: SelectionModel<Machine>;
cols = new Columns(
SELECT_COLUMN_NAME,
'id',
'namespace',
'createdAt',
'provider',
'status',
'history'
);
selected$ = new BehaviorSubject<Machine[]>([]);
status = repairer.RepairStatus;
columns$ = this.domainStoreService.getObjects('provider').pipe(
map((providers) =>
createGridColumns<Machine>([
'id',
{ header: 'Namespace', field: 'ns' },
createDatetimeFormattedColumn('created_at'),
createDescriptionFormattedColumn<Machine>(
'provider',
(data) =>
providers.find((p) => String(p.ref.id) === data.provider_id)?.data?.name,
(data) => data.provider_id
),
createTooltipTemplateGridColumn(
{
field: 'status',
formatter: (data: Machine) =>
getEnumKey(repairer.RepairStatus, data.status),
},
(d) => d.error_message
),
createTooltipTemplateGridColumn(
{
field: 'history',
formatter: (data: Machine) =>
data.history?.length ? String(data.history.length) : '',
},
(d) => d.history
),
])
),
shareReplay({ refCount: true, bufferSize: 1 })
);
cellTemplate: Record<string, TemplateRef<any>> = {};
constructor(
private machinesService: MachinesService,
@ -74,7 +106,8 @@ export class RepairingComponent implements OnInit {
private baseDialogService: BaseDialogService,
private repairManagementService: RepairManagementService,
private notificationService: NotificationService,
private notificationErrorService: NotificationErrorService
private notificationErrorService: NotificationErrorService,
private domainStoreService: DomainStoreService
) {}
ngOnInit() {
@ -90,7 +123,7 @@ export class RepairingComponent implements OnInit {
clean({
ids: splitIds(ids),
ns,
provider_id,
provider_id: isNil(provider_id) ? null : String(provider_id),
status,
error_message,
timespan:
@ -107,8 +140,9 @@ export class RepairingComponent implements OnInit {
.subscribe((params) => this.machinesService.search(params));
}
update() {
this.machinesService.refresh();
update(size: number) {
this.machinesService.refresh(size);
this.selected$.next([]);
}
fetchMore() {
@ -118,14 +152,14 @@ export class RepairingComponent implements OnInit {
repair() {
this.baseDialogService
.open(ConfirmActionDialogComponent, {
title: `Simple repair ${this.selection.selected.length} machines`,
title: `Simple repair ${this.selected$.value.length} machines`,
})
.afterClosed()
.pipe(
filter(({ status }) => status === BaseDialogResponseStatus.Success),
switchMap(() =>
this.repairManagementService.SimpleRepairAll(
this.selection.selected.map(({ id, ns }) => ({ id, ns }))
this.selected$.value.map(({ id, ns }) => ({ id, ns }))
)
),
untilDestroyed(this)
@ -140,9 +174,13 @@ export class RepairingComponent implements OnInit {
repairByScenario() {
this.baseDialogService
.open(RepairByScenarioDialogComponent, { machines: this.selection.selected })
.open(RepairByScenarioDialogComponent, { machines: this.selected$.value })
.afterClosed()
.pipe(untilDestroyed(this))
.subscribe();
}
trackById(index: number, item: Machine) {
return item.id;
}
}

View File

@ -15,8 +15,9 @@ import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActionsModule, BaseDialogModule } from '@vality/ng-core';
import { EnumKeyPipe, EnumKeysPipe } from '@cc/app/shared';
import { EnumKeyPipe, EnumKeysPipe, DomainObjectFieldComponent } from '@cc/app/shared';
import { MetadataFormModule } from '@cc/app/shared/components/metadata-form';
import { SimpleTableModule } from '@cc/components/simple-table';
import { EmptySearchResultModule } from '../../../components/empty-search-result';
import { TableModule } from '../../../components/table';
@ -50,6 +51,8 @@ import { RepairingComponent } from './repairing.component';
MatChipsModule,
EnumKeyPipe,
EnumKeysPipe,
DomainObjectFieldComponent,
SimpleTableModule,
],
declarations: [RepairingComponent, RepairByScenarioDialogComponent],
})

View File

@ -15,9 +15,9 @@ export class MachinesService extends PartialFetcher<Machine, SearchRequest> {
super();
}
protected fetch(params: SearchRequest, continuationToken: string) {
protected fetch(params: SearchRequest, continuationToken: string, size: number) {
return this.repairManagementService
.Search({ limit: 100, continuation_token: continuationToken, ...params })
.Search({ limit: size, continuation_token: continuationToken, ...params })
.pipe(
map(({ machines, continuation_token }) => ({
result: machines,

View File

@ -6,9 +6,9 @@
</cc-actions>
</div>
<cc-simple-table
[columns]="columns"
[data]="sources$ | async"
[inProgress]="!!(progress$ | async)"
[schema]="schema"
[loading]="!!(progress$ | async)"
noUpdate
></cc-simple-table>
</div>

View File

@ -1,8 +1,11 @@
import { Component } from '@angular/core';
import { StatSource } from '@vality/fistful-proto/internal/fistful_stat';
import { BaseDialogService } from '@vality/ng-core';
import { Schema } from '@cc/components/simple-table';
import {
createGridColumns,
createDescriptionFormattedColumn,
createDatetimeFormattedColumn,
} from '@cc/components/simple-table';
import { CreateSourceComponent } from './create-source/create-source.component';
import { FetchSourcesService } from './fetch-sources.service';
@ -21,11 +24,11 @@ import { FetchSourcesService } from './fetch-sources.service';
export class SourcesComponent {
sources$ = this.fetchSourcesService.sources$;
progress$ = this.fetchSourcesService.progress$;
schema = new Schema<StatSource>([
{ value: 'name', description: 'id' },
columns = createGridColumns([
createDescriptionFormattedColumn('name', 'id'),
'identity',
'currency_symbolic_code',
'created_at',
createDatetimeFormattedColumn('created_at'),
]);
constructor(

View File

@ -16,10 +16,10 @@
</mat-card>
<cc-simple-table
[columns]="columns"
[data]="wallets$ | async"
[hasMore]="hasMore$ | async"
[inProgress]="inProgress$ | async"
[schema]="schema"
[loading]="inProgress$ | async"
(fetchMore)="fetchMore()"
(size)="search($event)"
(update)="search($event.size)"

View File

@ -1,13 +1,16 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { StatWallet } from '@vality/fistful-proto/internal/fistful_stat';
import { clean } from '@vality/ng-core';
import { startWith, map } from 'rxjs/operators';
import { WalletParams } from '@cc/app/api/fistful-stat/query-dsl/types/wallet';
import { QueryParamsService } from '@cc/app/shared/services';
import { Schema } from '@cc/components/simple-table';
import {
createDatetimeFormattedColumn,
createDescriptionFormattedColumn,
createGridColumns,
} from '@cc/components/simple-table';
import { FetchWalletsService } from './fetch-wallets.service';
@ -29,11 +32,11 @@ export class WalletsComponent implements OnInit {
wallets$ = this.fetchWalletsService.searchResult$;
inProgress$ = this.fetchWalletsService.doAction$;
hasMore$ = this.fetchWalletsService.hasMore$;
schema = new Schema<StatWallet>([
{ value: 'name', description: 'id' },
columns = createGridColumns([
createDescriptionFormattedColumn('name', 'id'),
'currency_symbolic_code',
'identity_id',
{ value: 'created_at', type: 'datetime' },
createDatetimeFormattedColumn('created_at'),
]);
filters = this.fb.group<WalletParams>({
party_id: null,

View File

@ -0,0 +1,6 @@
<cc-select-search-field
[formControl]="control"
[label]="name | titlecase"
[options]="options$ | async"
[progress]="isLoading$ | async"
></cc-select-search-field>

View File

@ -0,0 +1,60 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnChanges } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { DomainObject } from '@vality/domain-proto/internal/domain';
import { defer, switchMap, ReplaySubject } from 'rxjs';
import { shareReplay, map } from 'rxjs/operators';
import { DomainStoreService } from '@cc/app/api/deprecated-damsel';
import { ComponentChanges } from '@cc/app/shared';
import {
DOMAIN_OBJECTS_TO_OPTIONS,
OtherDomainObjects,
defaultDomainObjectToOption,
} from '@cc/app/shared/services/domain-metadata-form-extensions/utils/domains-objects-to-options';
import { SelectSearchFieldModule } from '@cc/components/select-search-field';
import { ValidatedFormControlSuperclass, provideValueAccessor } from '@cc/utils';
@Component({
standalone: true,
selector: 'cc-domain-object-field',
templateUrl: './domain-object-field.component.html',
providers: [provideValueAccessor(() => DomainObjectFieldComponent)],
imports: [CommonModule, SelectSearchFieldModule, ReactiveFormsModule],
})
export class DomainObjectFieldComponent<T extends keyof DomainObject>
extends ValidatedFormControlSuperclass<DomainObject[T]>
implements OnChanges
{
@Input() name: T;
control = new FormControl<DomainObject[T]>(null);
options$ = defer(() => this.name$).pipe(
switchMap((name) => this.domainStoreService.getObjects(name)),
map((objs) => {
const domainObjectToOption =
this.name in DOMAIN_OBJECTS_TO_OPTIONS
? DOMAIN_OBJECTS_TO_OPTIONS[this.name as keyof OtherDomainObjects]
: defaultDomainObjectToOption;
return objs
.map(domainObjectToOption)
.map((o) => ({ ...o, description: `#${String(o.value)}` }));
}),
shareReplay({ bufferSize: 1, refCount: true })
);
isLoading$ = this.domainStoreService.isLoading$;
private name$ = new ReplaySubject<keyof DomainObject>(1);
constructor(private domainStoreService: DomainStoreService) {
super();
}
ngOnChanges(changes: ComponentChanges<DomainObjectFieldComponent<T>>) {
super.ngOnChanges(changes);
if (changes.name) {
this.name$.next(this.name);
}
}
}

View File

@ -0,0 +1 @@
export * from './domain-object-field.component';

View File

@ -8,3 +8,4 @@ export * from './shop-field';
export * from './shop-details';
export * from './payout-tool-details';
export * from './payout-tool-field';
export * from './domain-object-field';

View File

@ -95,8 +95,8 @@ export abstract class PartialFetcher<R, P> {
this.action$.next({ type: 'search', value, size });
}
refresh() {
this.action$.next({ type: 'search' });
refresh(size?: number) {
this.action$.next({ type: 'search', size });
}
fetchMore() {

View File

@ -1,4 +1,5 @@
@use '@angular/material' as mat;
@use '@ng-matero/extensions' as mtx;
@import '../../../components/components-themes';
@import '../../shared/components/shared-components-themes';
@ -14,6 +15,7 @@
@include mat.core-theme($theme);
@include mat.button-theme($theme);
@include mat.all-legacy-component-themes($theme);
@include mtx.all-component-themes($theme);
$foreground: map-get($theme, foreground);

View File

@ -21,7 +21,10 @@
*ngFor="let option of isExternalSearch ? options : (filteredOptions$ | async)"
[value]="option.value"
>
{{ option.label }}
<div class="label">{{ option.label }}</div>
<div *ngIf="option.description" class="mat-caption cc-secondary-text">
{{ option.description }}
</div>
</mat-option>
<mat-option
*ngIf="
@ -32,11 +35,14 @@
"
[value]="cachedOption.value"
>
{{ cachedOption.label }}
<div class="label">{{ cachedOption.label }}</div>
<div *ngIf="cachedOption.description" class="mat-caption cc-secondary-text">
{{ cachedOption.description }}
</div>
</mat-option>
</ng-container>
<ng-template #progressBar>
<mat-option disabled>
<mat-option disabled fxLayout="column" fxLayoutAlign="center none">
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</mat-option>
</ng-template>

View File

@ -1,3 +1,9 @@
mat-form-field {
width: 100%;
}
.label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
@ -22,6 +23,7 @@ import { SelectSearchFieldComponent } from './select-search-field.component';
MatSelectModule,
NgxMatSelectSearchModule,
MatProgressBarModule,
FlexModule,
],
declarations: [SelectSearchFieldComponent],
exports: [SelectSearchFieldComponent],

View File

@ -1,4 +1,5 @@
export interface Option<T> {
value: T;
label: string;
description?: string;
}

View File

@ -3,7 +3,10 @@ import { Option } from '../types';
const filterPredicate =
<T>(searchStr: string) =>
(option: Option<T>) =>
option.label.toLowerCase().includes(searchStr);
option.label.toLowerCase().includes(searchStr) ||
(option.description && option.description.toLowerCase().includes(searchStr)) ||
(typeof option.value !== 'object' &&
String(option.value).toLowerCase().includes(searchStr));
export const filterOptions = <T>(options: Option<T>[], controlValue: unknown): Option<T>[] =>
controlValue && typeof controlValue === 'string'

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'cc-simple-table-actions',
template: `
<div fxLayout fxLayoutGap="16px">
<ng-content></ng-content>
</div>
`,
})
export class SimpleTableActionsComponent {}

View File

@ -0,0 +1,39 @@
import { Component, ViewChild, TemplateRef, Output, EventEmitter } from '@angular/core';
import { createGridColumn, GridColumn } from '@cc/components/simple-table';
@Component({
selector: 'cc-simple-table-tooltip-cell-template',
template: `
<ng-template #tpl let-col="colDef" let-index="index" let-row>
<div
*ngIf="col.formatter ? col.formatter(row, col) : row[col.field] as val"
[matTooltip]="col._data?.tooltip && (col._data.tooltip(row) | json)"
[ngClass]="{ dashed: !!col._data?.tooltip?.(row) }"
matTooltipPosition="right"
>
{{ val }}
</div>
</ng-template>
`,
styles: [
`
.dashed {
text-decoration: underline;
cursor: default;
text-decoration-style: dotted;
}
`,
],
})
export class SimpleTableTooltipCellTemplateComponent {
@Output() template = new EventEmitter<TemplateRef<any>>();
@ViewChild('tpl', { static: true }) set tpl(tpl: TemplateRef<any>) {
this.template.emit(tpl);
}
}
export function createTooltipTemplateGridColumn<T>(col: GridColumn<T>, tooltip: (data: T) => any) {
return { ...createGridColumn(col), _data: { tooltip } };
}

View File

@ -1,3 +1,3 @@
export * from './types/schema';
export * from './simple-table.component';
export * from './simple-table.module';
export * from './utils/create-grid-columns';

View File

@ -1,14 +1,14 @@
<div fxLayout="column" fxLayoutGap="32px">
<ng-container *ngIf="data; else init">
<div *ngIf="!noUpdate || actions" fxLayout fxLayoutAlign="space-between" fxLayoutGap="8px">
<div *ngIf="!noUpdate" fxLayout fxLayoutGap="16px">
<button
[disabled]="inProgress"
[disabled]="loading"
mat-stroked-button
(click)="update.emit(this.size$.value ? { size: this.size$.value } : {})"
>
UPDATE
</button>
<button [disabled]="inProgress" [matMenuTriggerFor]="menu" mat-button>
<button [disabled]="loading" [matMenuTriggerFor]="menu" mat-button>
{{ size$ | async }} <mat-icon>table_rows_narrow</mat-icon>
</button>
<mat-menu #menu="matMenu">
@ -17,48 +17,29 @@
<button mat-menu-item (click)="size$.next(1000)">1000</button>
</mat-menu>
</div>
<ng-content select="cc-simple-table-actions"></ng-content>
</div>
<mat-card *ngIf="data.length; else empty" class="table-card">
<table [dataSource]="data" mat-table>
<ng-container *ngFor="let p of schema.params" [matColumnDef]="p.def">
<th *matHeaderCellDef mat-header-cell>{{ p.label }}</th>
<td *matCellDef="let i" mat-cell>
<ng-container [ngSwitch]="p.type">
<div>
<ng-template ngSwitchCase="datetime">
{{ p.value(i) | date : 'dd.MM.yyyy HH:mm:ss' }}
</ng-template>
<ng-template ngSwitchDefault>
{{ p.value(i) }}
</ng-template>
</div>
<div *ngIf="p.description" class="mat-caption cc-secondary-text">
{{ p.description(i) }}
</div>
</ng-container>
</td>
</ng-container>
<tr *matHeaderRowDef="schema.list" mat-header-row></tr>
<tr *matRowDef="let row; columns: schema.list" mat-row></tr>
</table>
<mat-card class="table-card">
<mtx-grid
[cellSelectable]="false"
[cellTemplate]="cellTemplate"
[columns]="columns"
[data]="data"
[loading]="loading || !columns"
[paginationTemplate]="footerTpl"
[rowSelectable]="rowSelectable"
[trackBy]="trackBy"
(rowSelectionChange)="rowSelectionChange.emit($event)"
></mtx-grid>
<ng-template #footerTpl>
<cc-show-more-button
*ngIf="hasMore"
[inProgress]="inProgress"
*ngIf="hasMore && columns"
[inProgress]="loading"
class="show-more"
style="width: 100%"
(fetchMore)="fetchMore.emit(this.size$.value ? { size: this.size$.value } : {})"
></cc-show-more-button>
</mat-card>
<ng-template #empty
><cc-empty-search-result style="width: 100%"></cc-empty-search-result
></ng-template>
</ng-container>
<ng-template #init>
<div *ngIf="inProgress" fxFlex fxLayout="row" fxLayoutAlign="center center">
<mat-spinner></mat-spinner>
</div>
</ng-template>
</ng-template>
</mat-card>
</div>

View File

@ -1,6 +1,10 @@
.table-card {
width: 100%;
overflow: hidden;
& > * {
border: none;
}
}
.show-more {

View File

@ -1,9 +1,11 @@
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { Component, Input, Output, EventEmitter, OnInit, ContentChild } from '@angular/core';
import { MtxGridColumn } from '@ng-matero/extensions/grid';
import { MtxGrid } from '@ng-matero/extensions/grid/grid';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { coerceBoolean } from 'coerce-property';
import { BehaviorSubject } from 'rxjs';
import { Schema } from './types/schema';
import { SimpleTableActionsComponent } from './components/simple-table-actions.component';
@UntilDestroy()
@Component({
@ -13,16 +15,22 @@ import { Schema } from './types/schema';
})
export class SimpleTableComponent<T> implements OnInit {
@Input() data: T[];
@Input() schema: Schema<T>;
@Input() columns: MtxGridColumn[];
@Input() cellTemplate?: MtxGrid['cellTemplate'];
@Input() trackBy?: MtxGrid['trackBy'];
@Input() @coerceBoolean loading = false;
@Input() @coerceBoolean rowSelectable = false;
@Output() rowSelectionChange = new EventEmitter<T[]>();
@Input() @coerceBoolean hasMore = false;
@Input() @coerceBoolean inProgress = false;
@Input() @coerceBoolean noUpdate = false;
@Output() size = new EventEmitter<number>();
@Output() update = new EventEmitter<{ size?: number }>();
@Output() fetchMore = new EventEmitter<{ size?: number }>();
@ContentChild(SimpleTableActionsComponent) actions: SimpleTableActionsComponent;
size$ = new BehaviorSubject<undefined | number>(25);
ngOnInit() {

View File

@ -9,10 +9,15 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MtxGridModule } from '@ng-matero/extensions/grid';
import { ActionsModule } from '@vality/ng-core';
import { TableModule } from '@cc/components/table';
import { EmptySearchResultModule } from '../empty-search-result';
import { SimpleTableActionsComponent } from './components/simple-table-actions.component';
import { SimpleTableTooltipCellTemplateComponent } from './components/simple-table-tooltip-cell-template.component';
import { SimpleTableComponent } from './simple-table.component';
@NgModule({
@ -29,8 +34,19 @@ import { SimpleTableComponent } from './simple-table.component';
MatIconModule,
MatMenuModule,
MatButtonModule,
ActionsModule,
MatTooltipModule,
MtxGridModule,
],
declarations: [
SimpleTableComponent,
SimpleTableActionsComponent,
SimpleTableTooltipCellTemplateComponent,
],
exports: [
SimpleTableComponent,
SimpleTableActionsComponent,
SimpleTableTooltipCellTemplateComponent,
],
declarations: [SimpleTableComponent],
exports: [SimpleTableComponent],
})
export class SimpleTableModule {}

View File

@ -1,56 +0,0 @@
import startCase from 'lodash-es/startCase';
import { Overwrite } from 'utility-types';
export type Path<T> = ((p: T) => string) | keyof T;
export interface BaseParam<T> {
def: string;
label: string;
value: (p: T) => string;
description?: (p: T) => string;
type?: 'datetime';
}
export type Param<T> = Overwrite<
Omit<BaseParam<T>, 'def'>,
{
label?: string;
value: Path<T>;
description?: Path<T>;
}
>;
function createGetValueFn<T>(v: ((d: T) => string) | keyof T): (d: T) => string {
if (typeof v === 'function') return v;
return (d) => d[v as any];
}
function createLabel(value: unknown): string {
return startCase(String(value));
}
export class Schema<T> {
params: BaseParam<T>[];
get list() {
return this.params.map((p) => p.def);
}
constructor(params: (Param<T> | keyof T)[]) {
this.params = params.map((p) => {
if (typeof p === 'object')
return {
def: p.label ?? String(p.value),
label: p.label ?? createLabel(p.value),
value: createGetValueFn(p.value),
description: p.description ? createGetValueFn(p.description) : null,
type: p?.type,
};
return {
def: String(p),
label: createLabel(p),
value: createGetValueFn(p),
};
});
}
}

View File

@ -0,0 +1,56 @@
import { formatDate } from '@angular/common';
import { MtxGridColumn } from '@ng-matero/extensions/grid';
import isObject from 'lodash-es/isObject';
import startCase from 'lodash-es/startCase';
import { Overwrite } from 'utility-types';
export type GridColumn<T> =
| (Overwrite<
MtxGridColumn,
{
formatter?: (rowData: T, colDef?: MtxGridColumn) => string;
}
> & {
_data?: any;
})
| keyof T;
export function createGridColumn<T>(col: GridColumn<T>) {
if (!isObject(col))
col = {
field: col as string,
};
if (!col.header) col.header = startCase(String(col.field).toLowerCase());
return {
...col,
};
}
export function createGridColumns<T>(columns: GridColumn<T>[]): MtxGridColumn[] {
return columns.map((col) => createGridColumn(col));
}
export function createDescriptionFormattedColumn<T>(
field: string,
getDescriptionOrDescriptionField: ((data: T) => string) | string,
getValue?: (data: T) => string
): MtxGridColumn {
return {
field,
formatter: (data: T) => {
const desc =
typeof getDescriptionOrDescriptionField === 'function'
? getDescriptionOrDescriptionField(data)
: String(data[getDescriptionOrDescriptionField]);
const value = getValue ? getValue(data) : String(data[field]);
return value + (desc ? `<div class="mat-caption cc-secondary-text">${desc}</div>` : '');
},
};
}
export function createDatetimeFormattedColumn<T>(field: string): MtxGridColumn {
return {
field,
formatter: (data: T) => formatDate(data[field], 'dd.MM.yyyy HH:mm:ss', 'en'),
};
}