IMP-165: Add support for the Wallet Manager role (#176)

This commit is contained in:
Rinat Arsaev 2024-02-27 16:43:34 +07:00 committed by GitHub
parent de7e8d242b
commit 9a718c491d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 168 additions and 45 deletions

View File

@ -4,5 +4,5 @@ runs:
using: composite
steps:
- uses: valitydev/action-frontend/setup@v0.1
- run: npm ci --force # TODO remove after bump ng 17.1
- run: npm ci
shell: bash

View File

@ -24,7 +24,7 @@ export class IsAccessAllowedPipe implements PipeTransform, OnDestroy {
transform(
roleAccessNames: RoleAccessName[] | keyof typeof RoleAccessName,
type: 'every' | 'some' = 'every',
type: 'every' | 'some' = 'some',
): boolean {
return this.asyncPipe.transform(
this.roleAccessService.isAccessAllowed(

View File

@ -23,7 +23,7 @@ export class RoleAccessService {
isAccessAllowed(
roleAccessNames: RoleAccessName[],
type: 'every' | 'some' = 'every',
type: 'every' | 'some' = 'some',
): Observable<boolean> {
if (!roleAccessNames.length) {
return of(true);

View File

@ -40,10 +40,7 @@ export class SelectRoleDialogComponent extends DialogSuperclass<
header: '',
formatter: (d) => (d ? roleAccessDict[d.name] : ''),
},
...this.roles
.filter((r) => r !== RoleId.WalletManager) // TODO: Remove when fix WalletManager role
.sort(sortRoleIds)
.map((r) => ({ field: r, header: roleIdDict[r] })),
...this.roles.sort(sortRoleIds).map((r) => ({ field: r, header: roleIdDict[r] })),
]),
);
data: NestedTableNode<RoleAccessGroup>[] = [

View File

@ -7,7 +7,7 @@
<ng-container *ngFor="let link of links">
<a
#rla="routerLinkActive"
*ngIf="link.roles | isAccessAllowed: 'some'"
*ngIf="link.roles | isAccessAllowed"
[active]="rla.isActive"
[routerLink]="link.path"
mat-tab-link

View File

@ -8,7 +8,7 @@
<ng-container *ngFor="let link of links">
<a
#rla="routerLinkActive"
*ngIf="link.roles | isAccessAllowed: 'some'"
*ngIf="link.roles | isAccessAllowed"
[active]="rla.isActive"
[routerLink]="link.path"
mat-tab-link

View File

@ -5,7 +5,7 @@
<ng-container *ngFor="let item of navbarItemConfig$ | async">
<dsh-navbar-item
#rla="routerLinkActive"
*ngIf="item.roles | isAccessAllowed: 'some'"
*ngIf="item.roles | isAccessAllowed"
[active]="rla.isActive"
[icon]="item.icon"
[routerLink]="item.routerLink"

View File

@ -11,6 +11,9 @@ export class ShopsBalanceService {
constructor(private analyticsService: AnalyticsService) {}
getBalances(shopIDs: string[]): Observable<ShopBalance[]> {
if (!shopIDs?.length) {
return of([]);
}
return this.analyticsService.getCurrentShopBalances({ shopIDs }).pipe(
map(
({ result }) =>

View File

@ -0,0 +1,62 @@
import { Injectable, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
import { combineLatest, switchMap, EMPTY, of } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { WalletsService } from '@dsh/app/api/wallet';
import { ShopsDataService } from '@dsh/app/shared';
export enum AppSection {
Payment,
Wallet,
}
const SECTION_URL_MAP = {
[AppSection.Payment]: 'payment-section',
[AppSection.Wallet]: 'wallet-section',
};
@Injectable({
providedIn: 'root',
})
export class SectionsService {
allowedMap$ = combineLatest([
this.shopsDataService.shopsAllowed$,
this.walletsService.wallets$.pipe(map((w) => !!w?.length)),
]).pipe(
map(([shopsAllowed, walletsAllowed]) => ({
[AppSection.Payment]: shopsAllowed || (!shopsAllowed && !walletsAllowed),
[AppSection.Wallet]: walletsAllowed,
})),
shareReplay({ refCount: true, bufferSize: 1 }),
);
constructor(
private shopsDataService: ShopsDataService,
private walletsService: WalletsService,
router: Router,
destroyRef: DestroyRef,
) {
this.allowedMap$
.pipe(
takeUntilDestroyed(destroyRef),
switchMap((allowedMap) => {
const activeSection: AppSection = Object.entries(SECTION_URL_MAP).find(
([, url]) => router.url.startsWith(`/${url}`),
)?.[0] as never;
if (!activeSection || allowedMap[activeSection]) {
return EMPTY;
}
return of(
Object.entries(allowedMap).find(
([, allowed]) => allowed,
)[0] as never as AppSection,
);
}),
)
.subscribe((activeSection) => {
void router.navigate([SECTION_URL_MAP[activeSection]]);
});
}
}

View File

@ -1,4 +1,5 @@
import { Inject, Injectable } from '@angular/core';
import { Inject, Injectable, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Wallet } from '@vality/swag-wallet';
import { ListWalletsRequestParams } from '@vality/swag-wallet/lib/api/wallets.service';
import { Observable } from 'rxjs';
@ -6,7 +7,7 @@ import { Observable } from 'rxjs';
import { WalletsService } from '@dsh/app/api/wallet';
import { mapToTimestamp, shareReplayRefCount } from '@dsh/app/custom-operators';
import { SEARCH_LIMIT } from '@dsh/app/sections/tokens';
import { FetchResult, PartialFetcher } from '@dsh/app/shared';
import { FetchResult, PartialFetcher, ContextOrganizationService } from '@dsh/app/shared';
@Injectable()
export class FetchWalletsService extends PartialFetcher<
@ -21,8 +22,15 @@ export class FetchWalletsService extends PartialFetcher<
constructor(
@Inject(SEARCH_LIMIT) private searchLimit: number,
private walletService: WalletsService,
private contextOrganizationService: ContextOrganizationService,
destroyRef: DestroyRef,
) {
super();
this.contextOrganizationService.organization$
.pipe(takeUntilDestroyed(destroyRef))
.subscribe(() => {
this.refresh();
});
}
protected fetch(

View File

@ -3,20 +3,20 @@ import { TranslocoService } from '@ngneat/transloco';
import { combineLatest, Observable } from 'rxjs';
import { map, shareReplay } from 'rxjs/operators';
import { WalletsService } from '@dsh/app/api/wallet';
import { SectionsService, AppSection } from '@dsh/app/sections/sections.service';
import { SectionLink } from './types';
@Injectable()
export class SectionsLinksService {
sectionLinks$: Observable<SectionLink[]> = combineLatest([
this.walletsService.wallets$.pipe(map((wallets) => !!wallets.length)),
this.sectionsService.allowedMap$,
// this.roleAccessService.isAccessAllowed([RoleAccessName.Claims]),
this.transloco.selectTranslation('services'),
]).pipe(
map(([hasWallets]) =>
map(([allowedMap]) =>
[
{
allowedMap[AppSection.Payment] && {
label: this.transloco.translate(
'sectionsLinks.links.payments',
null,
@ -24,7 +24,7 @@ export class SectionsLinksService {
),
path: `/payment-section`,
},
hasWallets && {
allowedMap[AppSection.Wallet] && {
label: this.transloco.translate(
'sectionsLinks.links.wallets',
null,
@ -42,7 +42,7 @@ export class SectionsLinksService {
);
constructor(
private walletsService: WalletsService,
private sectionsService: SectionsService,
private transloco: TranslocoService,
) {}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Shop } from '@vality/swag-payments';
import { Observable, Subject, of, repeat, merge, defer, first } from 'rxjs';
import { switchMap, filter, catchError } from 'rxjs/operators';
import { switchMap, filter, catchError, map } from 'rxjs/operators';
import { createTestShopClaimChangeset, ClaimsService } from '@dsh/app/api/claim-management';
import { ShopsService } from '@dsh/app/api/payments';
@ -14,17 +14,33 @@ import { IdGeneratorService } from '../id-generator';
providedIn: 'root',
})
export class ShopsDataService {
shops$: Observable<Shop[]> = merge(
shops$: Observable<Shop[]> = defer(() => this.shopsData$).pipe(
map((s) => (s ? s : [])),
shareReplayRefCount(),
);
shopsAllowed$ = defer(() => this.shopsData$).pipe(map(Boolean), shareReplayRefCount());
private reloadShops$ = new Subject<void>();
private shopsData$: Observable<Shop[] | null> = merge(
this.contextOrganizationService.organization$,
defer(() => this.reloadShops$),
).pipe(
switchMap(() => this.shopsService.getShopsForParty()),
switchMap((shops) => (shops.length ? of(shops) : this.createTestShop())),
switchMap(() =>
this.shopsService.getShopsForParty().pipe(
catchError((error) => {
if (error?.status == 401) {
return of(null);
}
throw error;
}),
),
),
switchMap((shops) =>
shops ? (shops.length ? of(shops) : this.createTestShop()) : of(null),
),
shareReplayRefCount(),
);
private reloadShops$ = new Subject<void>();
constructor(
private shopsService: ShopsService,
private contextOrganizationService: ContextOrganizationService,

View File

@ -1,4 +1,4 @@
<table [dataSource]="data" mat-table>
<table [dataSource]="dataSource" mat-table>
<ng-container
*ngFor="let column of columns; let columnIndex = index"
[matColumnDef]="column.field"

View File

@ -1,6 +1,8 @@
import { FlatTreeControl } from '@angular/cdk/tree';
import { Component, Input, TemplateRef } from '@angular/core';
import { Component, Input, TemplateRef, OnChanges, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatTreeFlattener, MatTreeFlatDataSource } from '@angular/material/tree';
import { ComponentChanges } from '@vality/ng-core';
import { of } from 'rxjs';
import { first } from 'rxjs/operators';
@ -50,41 +52,76 @@ const TREE_FLATTENER = new MatTreeFlattener<NestedTableNode, NestedTableFlatNode
templateUrl: 'nested-table.component.html',
styleUrls: ['nested-table.component.scss'],
})
export class NestedTableComponent {
@Input({
transform: (data: NestedTableNode[]) => {
const dataSource = new MatTreeFlatDataSource(TREE_CONTROL, TREE_FLATTENER);
dataSource.data = data || [];
dataSource
.connect({ viewChange: of() })
.pipe(first())
.subscribe((flatten) => {
for (const d of flatten) {
if (d.initExpanded) {
TREE_CONTROL.expand(d);
}
}
});
return dataSource;
},
})
data!: MatTreeFlatDataSource<NestedTableNode, NestedTableFlatNode>;
export class NestedTableComponent implements OnChanges {
@Input() data!: NestedTableNode[];
@Input() dataSource!: MatTreeFlatDataSource<NestedTableNode, NestedTableFlatNode>;
@Input() columns: NestedTableColumn[] = [];
@Input() cellsTemplates: Record<string, TemplateRef<unknown>> = {};
@Input() headersTemplates: Record<string, TemplateRef<unknown>> = {};
@Input() footerTemplates: Record<string, TemplateRef<unknown>> = {};
expanded!: Set<number>;
get displayedColumns() {
return (this.columns || []).map((c) => c.field);
}
constructor(private destroyRef: DestroyRef) {}
ngOnChanges(changes: ComponentChanges<NestedTableComponent>) {
if (changes.data && this.data?.length) {
this.dataSource = new MatTreeFlatDataSource(TREE_CONTROL, TREE_FLATTENER);
this.dataSource.data = this.data || [];
const initExpanded = !this.expanded;
if (initExpanded) {
this.expanded = new Set<number>();
}
this.getFlat().subscribe((flatten) => {
if (initExpanded) {
for (const [idx, d] of flatten.entries()) {
if (d.initExpanded) {
this.expanded.add(idx);
TREE_CONTROL.expand(d);
}
}
} else {
/**
* TODO: in this implementation it is expected that the table does not change
*/
for (const idx of this.expanded) {
const item = flatten[idx];
if (!item || !item.expandable) {
this.expanded.delete(idx);
continue;
}
TREE_CONTROL.expand(item);
}
}
});
}
}
toggle(data: NestedTableFlatNode) {
if (data.expandable) {
TREE_CONTROL.toggle(data);
this.getFlat().subscribe((flat) => {
const idx = flat.findIndex((f) => f === data);
if (TREE_CONTROL.isExpanded(data)) {
this.expanded.add(idx);
} else {
this.expanded.delete(idx);
}
});
}
}
isExpanded(data: NestedTableFlatNode) {
return TREE_CONTROL.isExpanded(data);
}
private getFlat() {
return this.dataSource
.connect({ viewChange: of() })
.pipe(first(), takeUntilDestroyed(this.destroyRef));
}
}