IMP-301: New shop selector (#381)

This commit is contained in:
Rinat Arsaev 2024-08-26 18:13:08 +05:00 committed by GitHub
parent 6a990a79ea
commit 55d2817cd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 204 additions and 131 deletions

View File

@ -18,8 +18,8 @@ import { ROUTING_CONFIG as REPAIRING_ROUTING_CONFIG } from './sections/repairing
import { ROUTING_CONFIG as PARTIES_ROUTING_CONFIG } from './sections/search-parties/routing-config';
import { SHOPS_ROUTING_CONFIG } from './sections/shops';
import { ROUTING_CONFIG as SOURCES_ROUTING_CONFIG } from './sections/sources/routing-config';
import { ROUTING_CONFIG as TARIFFS_ROUTING_CONFIG } from './sections/tariffs/routing-config';
import { ROUTING_CONFIG as TERMINALS_ROUTING_CONFIG } from './sections/terminals';
import { ROUTING_CONFIG as TERMS_ROUTING_CONFIG } from './sections/terms/routing-config';
import { ROUTING_CONFIG as WALLETS_ROUTING_CONFIG } from './sections/wallets/routing-config';
import { ROUTING_CONFIG as WITHDRAWALS_ROUTING_CONFIG } from './sections/withdrawals/routing-config';
import { SidenavInfoService } from './shared/components/sidenav-info';
@ -84,7 +84,7 @@ export class AppComponent {
{
label: 'Terms',
url: '/terms',
services: TARIFFS_ROUTING_CONFIG.services,
services: TERMS_ROUTING_CONFIG.services,
},
],
[

View File

@ -70,7 +70,7 @@ const ROUTES: Routes = [
},
{
path: 'terms',
loadChildren: () => import('./tariffs').then((m) => m.TariffsModule),
loadChildren: () => import('./terms').then((m) => m.Terms),
},
{
path: '404',

View File

@ -1 +0,0 @@
export * from './tariffs.module';

View File

@ -1,10 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ShopsTariffsComponent } from './components/shops-tariffs/shops-tariffs.component';
import { TariffsRoutingModule } from './tariffs-routing.module';
@NgModule({
imports: [CommonModule, TariffsRoutingModule, ShopsTariffsComponent],
})
export class TariffsModule {}

View File

@ -12,7 +12,7 @@ import {
getShopCashFlowSelectors,
isShopTermSetDecision,
SHOP_FEES_COLUMNS,
} from '../shops-tariffs/utils/shop-fees-columns';
} from '../shops-terms/utils/shop-fees-columns';
@Component({
selector: 'cc-shops-term-set-history-card',

View File

@ -21,7 +21,7 @@
[hasMore]="hasMore$ | async"
[maxSize]="250"
[progress]="isLoading$ | async"
[treeData]="tariffs$ | async"
[treeData]="terms$ | async"
(more)="more()"
(update)="update($event)"
></v-table2>

View File

@ -43,7 +43,7 @@ import {
import { getInlineDecisions2, InlineDecision2 } from '../../utils/get-inline-decisions';
import { ShopsTermSetHistoryCardComponent } from '../shops-term-set-history-card';
import { ShopsTariffsService } from './shops-tariffs.service';
import { ShopsTermsService } from './shops-terms.service';
import {
isShopTermSetDecision,
SHOP_FEES_COLUMNS,
@ -57,7 +57,7 @@ type Params = Pick<CommonSearchQueryParams, 'currencies'> &
>;
@Component({
selector: 'cc-shops-tariffs',
selector: 'cc-shops-terms',
standalone: true,
imports: [
CommonModule,
@ -73,9 +73,9 @@ type Params = Pick<CommonSearchQueryParams, 'currencies'> &
VSelectPipe,
MatTooltip,
],
templateUrl: './shops-tariffs.component.html',
templateUrl: './shops-terms.component.html',
})
export class ShopsTariffsComponent implements OnInit {
export class ShopsTermsComponent implements OnInit {
filtersForm = this.fb.group(
createControls<Params>({
currencies: null,
@ -85,7 +85,7 @@ export class ShopsTariffsComponent implements OnInit {
term_sets_ids: null,
}),
);
tariffs$ = this.shopsTariffsService.result$.pipe(
terms$ = this.shopsTermsService.result$.pipe(
map((terms) =>
terms.map((t) => ({
value: t,
@ -100,8 +100,8 @@ export class ShopsTariffsComponent implements OnInit {
})),
),
);
hasMore$ = this.shopsTariffsService.hasMore$;
isLoading$ = this.shopsTariffsService.isLoading$;
hasMore$ = this.shopsTermsService.hasMore$;
isLoading$ = this.shopsTermsService.isLoading$;
columns: Column2<ShopTermSet, InlineDecision2>[] = [
createShopColumn(
(d) => ({
@ -138,7 +138,7 @@ export class ShopsTariffsComponent implements OnInit {
private initFiltersValue = this.filtersForm.value;
constructor(
private shopsTariffsService: ShopsTariffsService,
private shopsTermsService: ShopsTermsService,
private fb: NonNullableFormBuilder,
private qp: QueryParamsService<Params>,
@Inject(DEBOUNCE_TIME_MS) private debounceTimeMs: number,
@ -158,7 +158,7 @@ export class ShopsTariffsComponent implements OnInit {
load(params: Params, options?: LoadOptions) {
const { currencies, term_sets_ids, ...otherParams } = params;
this.shopsTariffsService.load(
this.shopsTermsService.load(
clean({
common_search_query_params: { currencies },
term_sets_ids: term_sets_ids?.map?.((id) => ({ id })),
@ -169,10 +169,10 @@ export class ShopsTariffsComponent implements OnInit {
}
update(options?: UpdateOptions) {
this.shopsTariffsService.reload(options);
this.shopsTermsService.reload(options);
}
more() {
this.shopsTariffsService.more();
this.shopsTermsService.more();
}
}

View File

@ -14,7 +14,7 @@ import { DominatorService } from '@cc/app/api/dominator';
@Injectable({
providedIn: 'root',
})
export class ShopsTariffsService extends FetchSuperclass<ShopTermSet, ShopSearchQuery> {
export class ShopsTermsService extends FetchSuperclass<ShopTermSet, ShopSearchQuery> {
constructor(
private dominatorService: DominatorService,
private log: NotifyLogService,

View File

@ -18,7 +18,7 @@
wallets: arrayColumnTemplate
}"
[columns]="columns"
[data]="tariffs$ | async"
[data]="terms$ | async"
[hasMore]="hasMore$ | async"
[progress]="isLoading$ | async"
(more)="more()"

View File

@ -48,7 +48,7 @@ type Params = Pick<CommonSearchQueryParams, 'currencies'> &
>;
@Component({
selector: 'cc-terminals-tariffs',
selector: 'cc-terminals-terms',
standalone: true,
imports: [
CommonModule,
@ -73,7 +73,7 @@ export class TerminalsTermsComponent implements OnInit {
terminal_ids: null,
}),
);
tariffs$ = this.terminalsTermsService.result$;
terms$ = this.terminalsTermsService.result$;
hasMore$ = this.terminalsTermsService.hasMore$;
isLoading$ = this.terminalsTermsService.isLoading$;
columns: Column<TerminalTermSet>[] = [

View File

@ -12,7 +12,7 @@ import {
WALLET_FEES_COLUMNS,
isWalletTermSetDecision,
getWalletCashFlowSelectors,
} from '../wallets-tariffs/utils/wallet-fees-columns';
} from '../wallets-terms/utils/wallet-fees-columns';
@Component({
selector: 'cc-wallets-term-set-history-card',

View File

@ -18,7 +18,7 @@
[hasMore]="hasMore$ | async"
[maxSize]="250"
[progress]="isLoading$ | async"
[treeData]="tariffs$ | async"
[treeData]="terms$ | async"
(more)="more()"
(update)="update($event)"
></v-table2>

View File

@ -50,7 +50,7 @@ import {
getWalletCashFlowSelectors,
isWalletTermSetDecision,
} from './utils/wallet-fees-columns';
import { WalletsTariffsService } from './wallets-tariffs.service';
import { WalletsTermsService } from './wallets-terms.service';
type Params = Pick<CommonSearchQueryParams, 'currencies'> &
Overwrite<
@ -59,7 +59,7 @@ type Params = Pick<CommonSearchQueryParams, 'currencies'> &
>;
@Component({
selector: 'cc-wallets-tariffs',
selector: 'cc-wallets-terms',
standalone: true,
imports: [
CommonModule,
@ -75,9 +75,9 @@ type Params = Pick<CommonSearchQueryParams, 'currencies'> &
MatTooltip,
VSelectPipe,
],
templateUrl: './wallets-tariffs.component.html',
templateUrl: './wallets-terms.component.html',
})
export class WalletsTariffsComponent implements OnInit {
export class WalletsTermsComponent implements OnInit {
filtersForm = this.fb.group(
createControls<Params>({
currencies: null,
@ -88,7 +88,7 @@ export class WalletsTariffsComponent implements OnInit {
identity_ids: null,
}),
);
tariffs$ = this.walletsTariffsService.result$.pipe(
terms$ = this.walletsTermsService.result$.pipe(
map((terms) =>
terms.map((t) => ({
value: t,
@ -104,8 +104,8 @@ export class WalletsTariffsComponent implements OnInit {
})),
),
);
hasMore$ = this.walletsTariffsService.hasMore$;
isLoading$ = this.walletsTariffsService.isLoading$;
hasMore$ = this.walletsTermsService.hasMore$;
isLoading$ = this.walletsTermsService.isLoading$;
columns: Column2<WalletTermSet, InlineDecision2>[] = [
createWalletColumn((d) => ({ id: d.wallet_id, name: d.wallet_name, partyId: d.owner_id }), {
sticky: 'start',
@ -135,7 +135,7 @@ export class WalletsTariffsComponent implements OnInit {
private initFiltersValue = this.filtersForm.value;
constructor(
private walletsTariffsService: WalletsTariffsService,
private walletsTermsService: WalletsTermsService,
private fb: NonNullableFormBuilder,
private qp: QueryParamsService<Params>,
@Inject(DEBOUNCE_TIME_MS) private debounceTimeMs: number,
@ -155,7 +155,7 @@ export class WalletsTariffsComponent implements OnInit {
load(params: Params, options?: LoadOptions) {
const { currencies, term_sets_ids, identity_ids, ...otherParams } = params;
this.walletsTariffsService.load(
this.walletsTermsService.load(
clean({
common_search_query_params: { currencies },
term_sets_ids: term_sets_ids?.map?.((id) => ({ id })),
@ -167,10 +167,10 @@ export class WalletsTariffsComponent implements OnInit {
}
update(options?: UpdateOptions) {
this.walletsTariffsService.reload(options);
this.walletsTermsService.reload(options);
}
more() {
this.walletsTariffsService.more();
this.walletsTermsService.more();
}
}

View File

@ -17,7 +17,7 @@ import { DominatorService } from '@cc/app/api/dominator';
@Injectable({
providedIn: 'root',
})
export class WalletsTariffsService extends FetchSuperclass<WalletTermSet, WalletSearchQuery> {
export class WalletsTermsService extends FetchSuperclass<WalletTermSet, WalletSearchQuery> {
constructor(
private dominatorService: DominatorService,
private log: NotifyLogService,

View File

@ -0,0 +1 @@
export * from './terms';

View File

@ -3,28 +3,28 @@ import { RouterModule } from '@angular/router';
import { AppAuthGuardService } from '../../shared/services';
import { ShopsTariffsComponent } from './components/shops-tariffs/shops-tariffs.component';
import { ShopsTermsComponent } from './components/shops-terms/shops-terms.component';
import { TerminalsTermsComponent } from './components/terminals-terms/terminals-terms.component';
import { WalletsTariffsComponent } from './components/wallets-tariffs/wallets-tariffs.component';
import { WalletsTermsComponent } from './components/wallets-terms/wallets-terms.component';
import { ROUTING_CONFIG } from './routing-config';
import { TariffsComponent } from './tariffs.component';
import { TermsComponent } from './terms.component';
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: TariffsComponent,
component: TermsComponent,
canActivate: [AppAuthGuardService],
data: ROUTING_CONFIG,
children: [
{
path: 'shops',
component: ShopsTariffsComponent,
component: ShopsTermsComponent,
},
{
path: 'wallets',
component: WalletsTariffsComponent,
component: WalletsTermsComponent,
},
{
path: 'terminals',
@ -41,4 +41,4 @@ import { TariffsComponent } from './tariffs.component';
],
exports: [RouterModule],
})
export class TariffsRoutingModule {}
export class TermsRoutingModule {}

View File

@ -7,12 +7,12 @@ import { Link, NavComponent } from '@vality/ng-core';
import { SidenavInfoService } from '@cc/app/shared/components/sidenav-info';
@Component({
selector: 'cc-tariffs',
selector: 'cc-terms',
standalone: true,
imports: [CommonModule, RouterOutlet, MatSidenavModule, NavComponent],
templateUrl: './tariffs.component.html',
templateUrl: './terms.component.html',
})
export class TariffsComponent {
export class TermsComponent {
links: Link[] = [
{
label: 'Shops',

View File

@ -0,0 +1,10 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ShopsTermsComponent } from './components/shops-terms/shops-terms.component';
import { TermsRoutingModule } from './terms-routing.module';
@NgModule({
imports: [CommonModule, TermsRoutingModule, ShopsTermsComponent],
})
export class Terms {}

View File

@ -1,8 +1,13 @@
<mat-form-field>
<mat-label>{{ multiple ? 'Shops' : 'Shop' }}</mat-label>
<mat-select [formControl]="control" [multiple]="multiple" [required]="required">
<mat-option *ngFor="let shop of shops$ | async" [value]="shop.id">
{{ shop.details.name }}
</mat-option>
</mat-select>
</mat-form-field>
<v-select-field
[appearance]="appearance"
[formControl]="control"
[hint]="hint"
[label]="label || (multiple ? 'Shops' : 'Shop')"
[multiple]="multiple"
[options]="options$ | async"
[progress]="!!(progress$ | async)"
[required]="required"
[size]="size"
externalSearch
(searchChange)="this.searchChange$.next($event)"
></v-select-field>

View File

@ -1,8 +0,0 @@
:host {
overflow: hidden;
text-overflow: ellipsis;
}
mat-form-field {
width: 100%;
}

View File

@ -1,92 +1,160 @@
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnInit,
booleanAttribute,
DestroyRef,
inject,
AfterViewInit,
input,
Injector,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Shop } from '@vality/domain-proto/domain';
import { PartyID, ShopID } from '@vality/domain-proto/payment_processing';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import { PartyID, Shop, Party } from '@vality/domain-proto/internal/domain';
import { ShopID } from '@vality/domain-proto/payment_processing';
import {
createControlProviders,
FormControlSuperclass,
setDisabled,
isEmpty,
ComponentChanges,
debounceTimeWithFirst,
Option,
NotifyLogService,
progressTo,
} from '@vality/ng-core';
import { BehaviorSubject, defer, of } from 'rxjs';
import { filter, map, share, switchMap } from 'rxjs/operators';
import {
BehaviorSubject,
of,
Subject,
combineLatest,
Observable,
concat,
scheduled,
asyncScheduler,
concatMap,
} from 'rxjs';
import { map, switchMap, distinctUntilChanged, catchError } from 'rxjs/operators';
import { PartyManagementService } from '@cc/app/api/payment-processing';
import { DeanonimusService } from '../../../api/deanonimus';
import { PartyManagementService } from '../../../api/payment-processing';
import { DEBOUNCE_TIME_MS } from '../../../tokens';
@Component({
selector: 'cc-shop-field',
templateUrl: './shop-field.component.html',
styleUrls: ['./shop-field.component.scss'],
providers: createControlProviders(() => ShopFieldComponent),
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShopFieldComponent<M extends boolean = boolean>
extends FormControlSuperclass<
M extends true ? Shop[] : Shop,
M extends true ? ShopID[] : ShopID
>
implements OnChanges, OnInit
export class ShopFieldComponent
extends FormControlSuperclass<ShopID | ShopID[]>
implements AfterViewInit
{
@Input() partyId: PartyID;
@Input({ transform: booleanAttribute }) multiple: M;
@Input() label: string;
@Input({ transform: booleanAttribute }) required: boolean;
@Input() size?: string;
@Input() appearance?: string;
@Input() hint?: string;
@Input({ transform: booleanAttribute }) multiple = false;
partyId = input<PartyID>();
shops$ = defer(() => this.partyId$).pipe(
switchMap((partyId) =>
partyId
? this.partyManagementService
.Get(partyId)
.pipe(map(({ shops }) => Array.from(shops.values())))
: of<Shop[]>([]),
),
share(),
);
options$ = new BehaviorSubject<Option<ShopID>[]>([]);
searchChange$ = new Subject<string>();
progress$ = new BehaviorSubject(0);
private partyId$ = new BehaviorSubject<PartyID>(null);
private debounceTimeMs = inject(DEBOUNCE_TIME_MS);
constructor(
private partyManagementService: PartyManagementService,
private destroyRef: DestroyRef,
private deanonimusService: DeanonimusService,
private log: NotifyLogService,
private dr: DestroyRef,
private injector: Injector,
) {
super();
}
setDisabledState(isDisabled: boolean) {
super.setDisabledState(!this.partyId || isDisabled);
}
ngOnChanges(changes: ComponentChanges<ShopFieldComponent>): void {
super.ngOnChanges(changes);
if (changes.partyId && this.partyId !== this.partyId$.value) {
this.partyId$.next(changes.partyId.currentValue);
setDisabled(this.control, !this.partyId);
}
}
ngOnInit() {
this.shops$
.pipe(
filter(
(shops) =>
!isEmpty(this.control.value) &&
(Array.isArray(this.control.value)
? !this.control.value.every((v) => shops.some((s) => s.id === v))
: !shops.some((s) => s.id === this.control.value)),
ngAfterViewInit() {
const initValues = this.getCurrentValues();
combineLatest([
concat(
scheduled(initValues.length ? initValues : [''], asyncScheduler),
this.searchChange$.pipe(
distinctUntilChanged(),
debounceTimeWithFirst(this.debounceTimeMs),
),
takeUntilDestroyed(this.destroyRef),
),
toObservable(this.partyId, { injector: this.injector }).pipe(
switchMap((partyId) =>
partyId
? this.partyManagementService.Get(partyId).pipe(progressTo(this.progress$))
: of(null),
),
),
])
.pipe(
concatMap(([term, party]) =>
party
? of(this.searchShopsByParty(party, term))
: this.searchShops(term).pipe(progressTo(this.progress$)),
),
takeUntilDestroyed(this.dr),
)
.subscribe(() => {
this.control.setValue(null);
.subscribe((options) => {
const oldOptions = this.options$.value;
this.options$.next(
this.getCurrentValues().reduce((acc, v) => {
if (acc.every((o) => o.value !== v)) {
acc.push(
oldOptions.find((f) => f.value === v) ?? {
label: `#${v}`,
value: v,
description: v,
},
);
}
return acc;
}, options),
);
});
super.ngOnInit();
}
private searchShops(search: string): Observable<Option<ShopID>[]> {
return this.deanonimusService.searchShopText(search).pipe(
map((partyShops) =>
partyShops.map((p) => ({
label: p.shop.details.name,
value: p.shop.id,
description: p.shop.id,
})),
),
catchError((err) => {
this.log.error(err, 'Search error');
return of([]);
}),
);
}
private searchShopsByParty(party: Party, search: string): Option<ShopID>[] {
const searchStr = search.trim().toLowerCase();
return Array.from(party.shops.values())
.map((shop) => ({ party, shop }))
.sort(
(a, b) =>
+this.includeSearchStr(a, searchStr) - +this.includeSearchStr(b, searchStr),
)
.map((p) => ({
label: p.shop.details.name,
value: p.shop.id,
description: p.shop.id,
}));
}
private includeSearchStr({ shop, party }: { party: Party; shop: Shop }, searchStr: string) {
return [shop.id, shop.details.name, party.id, party.party_name].some((v) =>
String(v ?? '')
.toLowerCase()
.includes(searchStr),
);
}
private getCurrentValues() {
const v = this.control.value;
return v ? (Array.isArray(v) ? v : [v]) : [];
}
}

View File

@ -3,12 +3,20 @@ import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { SelectFieldModule } from '@vality/ng-core';
import { ShopFieldComponent } from './shop-field.component';
@NgModule({
declarations: [ShopFieldComponent],
imports: [CommonModule, MatFormFieldModule, MatSelectModule, FormsModule, ReactiveFormsModule],
imports: [
CommonModule,
MatFormFieldModule,
MatSelectModule,
FormsModule,
ReactiveFormsModule,
SelectFieldModule,
],
exports: [ShopFieldComponent],
})
export class ShopFieldModule {}