mirror of
https://github.com/valitydev/dashboard.git
synced 2024-11-06 02:25:23 +00:00
IMP-165: Add support for the Wallet Manager role (#176)
This commit is contained in:
parent
de7e8d242b
commit
9a718c491d
2
.github/actions/init/action.yaml
vendored
2
.github/actions/init/action.yaml
vendored
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
|
@ -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>[] = [
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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 }) =>
|
||||
|
62
src/app/sections/sections.service.ts
Normal file
62
src/app/sections/sections.service.ts
Normal 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]]);
|
||||
});
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user