diff --git a/.github/actions/init/action.yaml b/.github/actions/init/action.yaml index 76395eea..61ecb8b1 100644 --- a/.github/actions/init/action.yaml +++ b/.github/actions/init/action.yaml @@ -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 diff --git a/src/app/auth/is-access-allowed.pipe.ts b/src/app/auth/is-access-allowed.pipe.ts index f216cbd5..3a5c9f63 100644 --- a/src/app/auth/is-access-allowed.pipe.ts +++ b/src/app/auth/is-access-allowed.pipe.ts @@ -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( diff --git a/src/app/auth/role-access.service.ts b/src/app/auth/role-access.service.ts index edf6322a..ef9237e8 100644 --- a/src/app/auth/role-access.service.ts +++ b/src/app/auth/role-access.service.ts @@ -23,7 +23,7 @@ export class RoleAccessService { isAccessAllowed( roleAccessNames: RoleAccessName[], - type: 'every' | 'some' = 'every', + type: 'every' | 'some' = 'some', ): Observable { if (!roleAccessNames.length) { return of(true); diff --git a/src/app/sections/organization-section/organization-details/change-roles-table/components/select-role-dialog/select-role-dialog.component.ts b/src/app/sections/organization-section/organization-details/change-roles-table/components/select-role-dialog/select-role-dialog.component.ts index cc7016fa..b6ef75cf 100644 --- a/src/app/sections/organization-section/organization-details/change-roles-table/components/select-role-dialog/select-role-dialog.component.ts +++ b/src/app/sections/organization-section/organization-details/change-roles-table/components/select-role-dialog/select-role-dialog.component.ts @@ -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[] = [ diff --git a/src/app/sections/payment-section/integrations/integrations.component.html b/src/app/sections/payment-section/integrations/integrations.component.html index fcbf3da3..0b0515cb 100644 --- a/src/app/sections/payment-section/integrations/integrations.component.html +++ b/src/app/sections/payment-section/integrations/integrations.component.html @@ -7,7 +7,7 @@ { + if (!shopIDs?.length) { + return of([]); + } return this.analyticsService.getCurrentShopBalances({ shopIDs }).pipe( map( ({ result }) => diff --git a/src/app/sections/sections.service.ts b/src/app/sections/sections.service.ts new file mode 100644 index 00000000..e7e6f50a --- /dev/null +++ b/src/app/sections/sections.service.ts @@ -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]]); + }); + } +} diff --git a/src/app/sections/wallet-section/wallets/services/fetch-wallets/fetch-wallets.service.ts b/src/app/sections/wallet-section/wallets/services/fetch-wallets/fetch-wallets.service.ts index 128b5af9..9478e6bd 100644 --- a/src/app/sections/wallet-section/wallets/services/fetch-wallets/fetch-wallets.service.ts +++ b/src/app/sections/wallet-section/wallets/services/fetch-wallets/fetch-wallets.service.ts @@ -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( diff --git a/src/app/shared/services/sections-links/section-links.service.ts b/src/app/shared/services/sections-links/section-links.service.ts index 3e3e03b9..f4d2725c 100644 --- a/src/app/shared/services/sections-links/section-links.service.ts +++ b/src/app/shared/services/sections-links/section-links.service.ts @@ -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 = 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, ) {} } diff --git a/src/app/shared/services/shops-data/shops-data.service.ts b/src/app/shared/services/shops-data/shops-data.service.ts index 05ceed18..57b09742 100644 --- a/src/app/shared/services/shops-data/shops-data.service.ts +++ b/src/app/shared/services/shops-data/shops-data.service.ts @@ -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 = merge( + shops$: Observable = defer(() => this.shopsData$).pipe( + map((s) => (s ? s : [])), + shareReplayRefCount(), + ); + shopsAllowed$ = defer(() => this.shopsData$).pipe(map(Boolean), shareReplayRefCount()); + + private reloadShops$ = new Subject(); + private shopsData$: Observable = 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(); - constructor( private shopsService: ShopsService, private contextOrganizationService: ContextOrganizationService, diff --git a/src/components/nested-table/nested-table.component.html b/src/components/nested-table/nested-table.component.html index fc68c5f7..36e47940 100644 --- a/src/components/nested-table/nested-table.component.html +++ b/src/components/nested-table/nested-table.component.html @@ -1,4 +1,4 @@ - +
{ - 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; +export class NestedTableComponent implements OnChanges { + @Input() data!: NestedTableNode[]; + @Input() dataSource!: MatTreeFlatDataSource; @Input() columns: NestedTableColumn[] = []; @Input() cellsTemplates: Record> = {}; @Input() headersTemplates: Record> = {}; @Input() footerTemplates: Record> = {}; + expanded!: Set; + get displayedColumns() { return (this.columns || []).map((c) => c.field); } + constructor(private destroyRef: DestroyRef) {} + + ngOnChanges(changes: ComponentChanges) { + 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(); + } + 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)); + } }