active org in url

This commit is contained in:
Rinat Arsaev 2021-12-03 14:55:53 +03:00
parent 2c6c392b82
commit b4268b9ff1
32 changed files with 396 additions and 307 deletions

View File

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { IdGeneratorService } from '@rbkmoney/id-generator';
import { Observable } from 'rxjs';
import { first, switchMap } from 'rxjs/operators';
import {
Claim,
@ -11,105 +10,71 @@ import {
Reason,
StatusModificationUnit,
} from '@dsh/api-codegen/claim-management';
import { ContextService } from '@dsh/app/shared/services/context';
import { mapResult, noContinuationToken } from '@dsh/operators';
export const CLAIM_STATUS = StatusModificationUnit.StatusEnum;
@Injectable()
export class ClaimsService {
constructor(
private claimsService: APIClaimsService,
private idGenerator: IdGeneratorService,
private contextService: ContextService
) {}
constructor(private claimsService: APIClaimsService, private idGenerator: IdGeneratorService) {}
searchClaims(
partyId: string,
limit: number,
claimStatuses?: StatusModificationUnit.StatusEnum[],
claimID?: number,
continuationToken?: string
): Observable<InlineResponse200> {
return this.contextService.organization$.pipe(
first(),
switchMap((organization) =>
this.claimsService.searchClaims(
this.idGenerator.shortUuid(),
organization.id,
limit,
undefined,
continuationToken,
claimID,
claimStatuses || Object.values(StatusModificationUnit.StatusEnum)
)
)
return this.claimsService.searchClaims(
this.idGenerator.shortUuid(),
partyId,
limit,
undefined,
continuationToken,
claimID,
claimStatuses || Object.values(StatusModificationUnit.StatusEnum)
);
}
search1000Claims(claimStatuses?: StatusModificationUnit.StatusEnum[]): Observable<Claim[]> {
return this.searchClaims(1000, claimStatuses).pipe(noContinuationToken, mapResult);
search1000Claims(partyId: string, claimStatuses?: StatusModificationUnit.StatusEnum[]): Observable<Claim[]> {
return this.searchClaims(partyId, 1000, claimStatuses).pipe(noContinuationToken, mapResult);
}
getClaimByID(claimID: number): Observable<Claim> {
return this.contextService.organization$.pipe(
first(),
switchMap((organization) =>
this.claimsService.getClaimByID(this.idGenerator.shortUuid(), organization.id, claimID)
)
getClaimByID(partyId: string, claimID: number): Observable<Claim> {
return this.claimsService.getClaimByID(this.idGenerator.shortUuid(), partyId, claimID);
}
createClaim(partyId: string, changeset: Modification[]): Observable<Claim> {
return this.claimsService.createClaim(this.idGenerator.shortUuid(), partyId, changeset);
}
updateClaimByID(
partyId: string,
claimID: number,
claimRevision: number,
changeset: Modification[]
): Observable<void> {
return this.claimsService.updateClaimByID(
this.idGenerator.shortUuid(),
partyId,
claimID,
claimRevision,
changeset
);
}
createClaim(changeset: Modification[]): Observable<Claim> {
return this.contextService.organization$.pipe(
first(),
switchMap((organization) =>
this.claimsService.createClaim(this.idGenerator.shortUuid(), organization.id, changeset)
)
revokeClaimByID(partyId: string, claimID: number, claimRevision: number, reason: Reason): Observable<void> {
return this.claimsService.revokeClaimByID(
this.idGenerator.shortUuid(),
partyId,
claimID,
claimRevision,
undefined,
reason
);
}
updateClaimByID(claimID: number, claimRevision: number, changeset: Modification[]): Observable<void> {
return this.contextService.organization$.pipe(
first(),
switchMap((organization) =>
this.claimsService.updateClaimByID(
this.idGenerator.shortUuid(),
organization.id,
claimID,
claimRevision,
changeset
)
)
);
}
revokeClaimByID(claimID: number, claimRevision: number, reason: Reason): Observable<void> {
return this.contextService.organization$.pipe(
first(),
switchMap((organization) =>
this.claimsService.revokeClaimByID(
this.idGenerator.shortUuid(),
organization.id,
claimID,
claimRevision,
undefined,
reason
)
)
);
}
requestReviewClaimByID(claimID: number, claimRevision: number): Observable<void> {
return this.contextService.organization$.pipe(
first(),
switchMap((organization) =>
this.claimsService.requestReviewClaimByID(
this.idGenerator.shortUuid(),
organization.id,
claimID,
claimRevision
)
)
);
requestReviewClaimByID(partyId: string, claimID: number, claimRevision: number): Observable<void> {
return this.claimsService.requestReviewClaimByID(this.idGenerator.shortUuid(), partyId, claimID, claimRevision);
}
}

View File

@ -1,5 +1,2 @@
<dsh-home>
<dsh-sections *ngIf="bootstrapped$ | async"></dsh-sections>
</dsh-home>
<dsh-feedback fxHide.lt-md></dsh-feedback>
<router-outlet></router-outlet>
<dsh-yandex-metrika *ngIf="env.production"></dsh-yandex-metrika>

View File

@ -1,31 +1,12 @@
import { Component, Inject, OnInit } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import * as Sentry from '@sentry/angular';
import { first } from 'rxjs/operators';
import { Component, Inject, ChangeDetectionStrategy } from '@angular/core';
import { ENV, Env } from '../environments';
import { BootstrapService } from './bootstrap.service';
import { KeycloakTokenInfoService } from './shared';
@UntilDestroy()
@Component({
selector: 'dsh-root',
templateUrl: 'app.component.html',
providers: [BootstrapService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnInit {
bootstrapped$ = this.bootstrapService.bootstrapped$;
constructor(
private bootstrapService: BootstrapService,
@Inject(ENV) public env: Env,
private keycloakTokenInfoService: KeycloakTokenInfoService
) {}
ngOnInit(): void {
this.bootstrapService.bootstrap();
this.keycloakTokenInfoService.partyID$
.pipe(first(), untilDestroyed(this))
.subscribe((partyID) => Sentry.setUser({ id: partyID }));
}
export class AppComponent {
constructor(@Inject(ENV) public env: Env) {}
}

View File

@ -16,7 +16,6 @@ import { TRANSLOCO_CONFIG, TRANSLOCO_LOADER, TranslocoConfig, TranslocoModule }
import * as Sentry from '@sentry/angular';
import { ErrorModule, KeycloakTokenInfoModule } from '@dsh/app/shared/services';
import { ContextModule } from '@dsh/app/shared/services/context';
import { QUERY_PARAMS_SERIALIZERS } from '@dsh/app/shared/services/query-params/utils/query-params-serializers';
import { createDateRangeWithPresetSerializer } from '@dsh/components/filters/date-range-filter';
import { SELECT_SEARCH_FIELD_OPTIONS } from '@dsh/components/form-controls/select-search-field';
@ -41,7 +40,6 @@ import { TranslocoHttpLoaderService } from './transloco-http-loader.service';
import { YandexMetrikaConfigService, YandexMetrikaModule } from './yandex-metrika';
@NgModule({
declarations: [AppComponent],
imports: [
CommonModule,
BrowserModule,
@ -63,7 +61,6 @@ import { YandexMetrikaConfigService, YandexMetrikaModule } from './yandex-metrik
IconsModule,
KeycloakTokenInfoModule,
FlexLayoutModule,
ContextModule,
],
providers: [
LanguageService,
@ -133,6 +130,7 @@ import { YandexMetrikaConfigService, YandexMetrikaModule } from './yandex-metrik
useValue: [createDateRangeWithPresetSerializer()],
},
],
declarations: [AppComponent],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@ -81,16 +81,21 @@ export class BootstrapService {
}
private createTestShop(): Observable<boolean> {
return this.claimsService
.createClaim(
createTestShopClaimChangeset(
this.idGenerator.uuid(),
this.idGenerator.uuid(),
this.idGenerator.uuid(),
this.idGenerator.uuid()
return this.contextService.organization$.pipe(
first(),
switchMap((org) =>
this.claimsService.createClaim(
org.id,
createTestShopClaimChangeset(
this.idGenerator.uuid(),
this.idGenerator.uuid(),
this.idGenerator.uuid(),
this.idGenerator.uuid()
)
)
)
.pipe(mapTo(true));
),
mapTo(true)
);
}
private initContext(): Observable<boolean> {

View File

@ -18,7 +18,7 @@
dsh-button
color="accent"
(click)="confirm()"
[disabled]="!selectedOrganization || selectedOrganization.id === (contextOrganization$ | async)?.id"
[disabled]="!selectedOrganization || selectedOrganization.id === organization.id"
>
{{ t('confirm') }}
</button>

View File

@ -1,14 +1,11 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { ChangeDetectionStrategy, Component, OnInit, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import { combineLatest } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { Organization } from '@dsh/api-codegen/organizations';
import { SEARCH_LIMIT } from '@dsh/app/sections/tokens';
import { BaseDialogResponseStatus } from '@dsh/app/shared/components/dialog/base-dialog';
import { ContextService } from '@dsh/app/shared/services/context';
import { FetchOrganizationsService } from '@dsh/app/shared/services/fetch-organizations';
const DISPLAYED_COUNT = 5;
@ -26,24 +23,22 @@ export class SelectActiveOrganizationDialogComponent implements OnInit {
displayedCount = DISPLAYED_COUNT;
selectedOrganization: Organization;
isLoading$ = this.fetchOrganizationsService.doAction$;
contextOrganization$ = this.contextService.organization$;
constructor(
private dialogRef: MatDialogRef<
SelectActiveOrganizationDialogComponent,
BaseDialogResponseStatus | Organization
>,
private fetchOrganizationsService: FetchOrganizationsService,
private router: Router,
private contextService: ContextService
@Inject(MAT_DIALOG_DATA) public organization: Organization,
private fetchOrganizationsService: FetchOrganizationsService
) {}
ngOnInit(): void {
this.fetchOrganizationsService.search();
combineLatest([this.organizations$, this.contextService.organization$])
this.organizations$
.pipe(
first(),
map(([orgs, activeOrg]) => orgs.find((org) => org.id === activeOrg.id)),
map((organizations) => organizations.find((org) => org.id === this.organization.id)),
untilDestroyed(this)
)
.subscribe((organization) => (this.selectedOrganization = organization));
@ -51,7 +46,6 @@ export class SelectActiveOrganizationDialogComponent implements OnInit {
confirm(): void {
this.dialogRef.close(this.selectedOrganization);
void this.router.navigate(['/']);
}
close(): void {

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, Output, EventEmitter, Inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { filter } from 'rxjs/operators';
import { filter, first, switchMap } from 'rxjs/operators';
import { Organization } from '@dsh/api-codegen/organizations';
import { DIALOG_CONFIG, DialogConfig } from '@dsh/app/sections/tokens';
@ -57,13 +57,20 @@ export class UserComponent {
selectActiveOrg(): void {
this.selected.emit();
this.dialog
.open<SelectActiveOrganizationDialogComponent, void, BaseDialogResponseStatus | Organization>(
SelectActiveOrganizationDialogComponent,
this.dialogConfig.medium
this.contextService.organization$
.pipe(
first(),
switchMap((organization) =>
this.dialog
.open<
SelectActiveOrganizationDialogComponent,
Organization,
BaseDialogResponseStatus | Organization
>(SelectActiveOrganizationDialogComponent, { ...this.dialogConfig.medium, data: organization })
.afterClosed()
),
filter((res) => !Object.values(BaseDialogResponseStatus).includes(res as BaseDialogResponseStatus))
)
.afterClosed()
.pipe(filter((res) => !Object.values(BaseDialogResponseStatus).includes(res as BaseDialogResponseStatus)))
.subscribe((org: Organization) => {
this.contextService.switchOrganization(org.id);
});

View File

@ -1,20 +1,18 @@
<div *ngIf="routerNavigationEnd$ | async">
<dsh-welcome-image *ngIf="hasBackground"></dsh-welcome-image>
<ng-container *ngTemplateOutlet="(isXSmallSmall$ | async) ? mobile : laptop"> </ng-container>
</div>
<dsh-welcome-image *ngIf="hasBackground$ | async"></dsh-welcome-image>
<ng-container *ngTemplateOutlet="(isXSmallSmall$ | async) ? mobile : laptop"> </ng-container>
<ng-template #content>
<ng-content></ng-content>
</ng-template>
<ng-template #mobile>
<dsh-mobile-grid [inverted]="hasBackground" [logoName]="logoName">
<dsh-mobile-grid [inverted]="hasBackground$ | async" [logoName]="logoName">
<ng-container *ngTemplateOutlet="content"></ng-container>
</dsh-mobile-grid>
</ng-template>
<ng-template #laptop>
<dsh-laptop-grid [inverted]="hasBackground" [logoName]="logoName">
<dsh-laptop-grid [inverted]="hasBackground$ | async" [logoName]="logoName">
<ng-container *ngTemplateOutlet="content"></ng-container>
</dsh-laptop-grid>
</ng-template>

View File

@ -1,9 +1,10 @@
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { Component, OnInit } from '@angular/core';
import { NavigationEnd, Router, RouterEvent } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Observable } from 'rxjs';
import { filter, map, pluck, take } from 'rxjs/operators';
import { Component } from '@angular/core';
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import { UntilDestroy } from '@ngneat/until-destroy';
import { pluck, filter, map, startWith, distinctUntilChanged } from 'rxjs/operators';
import { shareReplayRefCount } from '@dsh/operators';
import { ThemeManager } from '../theme-manager';
@ -12,34 +13,25 @@ import { ThemeManager } from '../theme-manager';
selector: 'dsh-home',
templateUrl: 'home.component.html',
})
export class HomeComponent implements OnInit {
routerNavigationEnd$: Observable<boolean>;
isXSmallSmall$: Observable<boolean>;
export class HomeComponent {
isXSmallSmall$ = this.breakpointObserver.observe([Breakpoints.XSmall, Breakpoints.Small]).pipe(pluck('matches'));
hasBackground$ = this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
startWith(null),
map(() => /^\/organization\/[\w-]+$/.test(this.router.url) && this.themeManager.isMainBackgroundImages),
distinctUntilChanged(),
shareReplayRefCount()
);
constructor(
private router: Router,
private route: ActivatedRoute,
// need to create class when home component was init
private themeManager: ThemeManager,
private breakpointObserver: BreakpointObserver
) {}
get hasBackground(): boolean {
return this.router.url === '/' && this.themeManager.isMainBackgroundImages;
}
get logoName(): string {
return this.themeManager.logoName;
}
ngOnInit(): void {
this.routerNavigationEnd$ = this.router.events.pipe(
filter((event: RouterEvent) => event instanceof NavigationEnd),
map(() => true),
take(1),
untilDestroyed(this)
);
this.isXSmallSmall$ = this.breakpointObserver
.observe([Breakpoints.XSmall, Breakpoints.Small])
.pipe(pluck('matches'));
}
}

View File

@ -11,7 +11,7 @@
<a
*ngFor="let link of sectionLinks$ | async"
mat-tab-link
[routerLink]="link.path"
[routerLink]="'/organization/' + (organization$ | async)?.id + link.path"
routerLinkActive
[routerLinkActiveOptions]="{ exact: link?.exact }"
#rla="routerLinkActive"

View File

@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { ContextService } from '@dsh/app/shared/services/context';
import { SectionsLinksService } from '@dsh/app/shared/services/sections-links';
import { coerceBoolean } from '@dsh/utils';
@ -14,6 +15,7 @@ export class ToolbarComponent {
@Input() logoName: string;
sectionLinks$ = this.sectionsLinksService.sectionLinks$;
organization$ = this.contextService.organization$;
constructor(private sectionsLinksService: SectionsLinksService) {}
constructor(private sectionsLinksService: SectionsLinksService, private contextService: ContextService) {}
}

View File

@ -4,9 +4,10 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { catchError, filter, map, pluck, shareReplay, switchMap, tap } from 'rxjs/operators';
import { catchError, filter, map, pluck, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { ClaimsService } from '@dsh/api/claims';
import { ContextService } from '@dsh/app/shared/services/context';
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
import { UiError } from '../../ui-error';
@ -39,7 +40,8 @@ export class ReviewClaimService {
private receiveClaimService: ReceiveClaimService,
private dialog: MatDialog,
private snackBar: MatSnackBar,
private transloco: TranslocoService
private transloco: TranslocoService,
private contextService: ContextService
) {
this.reviewClaim$
.pipe(
@ -61,8 +63,9 @@ export class ReviewClaimService {
)
),
switchMap(() => this.routeParamClaimService.claim$),
switchMap(({ id, revision }) =>
this.claimsApiService.requestReviewClaimByID(id, revision).pipe(
withLatestFrom(this.contextService.organization$),
switchMap(([{ id, revision }, org]) =>
this.claimsApiService.requestReviewClaimByID(org.id, id, revision).pipe(
catchError((ex) => {
this.progress$.next(false);
console.error(ex);

View File

@ -4,9 +4,10 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { progress } from '@rbkmoney/utils';
import get from 'lodash-es/get';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { catchError, filter, pluck, switchMap, tap } from 'rxjs/operators';
import { catchError, filter, pluck, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { ClaimsService } from '@dsh/api/claims';
import { ContextService } from '@dsh/app/shared/services/context';
import { UiError } from '../../../ui-error';
import { RevokeClaimDialogComponent } from './revoke-claim-dialog.component';
@ -27,7 +28,8 @@ export class RevokeClaimDialogService {
private dialogRef: MatDialogRef<RevokeClaimDialogComponent, 'cancel' | 'revoked'>,
private claimsApiService: ClaimsService,
private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA) private data: { claimId: number; revision: number }
@Inject(MAT_DIALOG_DATA) private data: { claimId: number; revision: number },
private contextService: ContextService
) {
this.form = this.fb.group({
reason: ['', [Validators.required, Validators.maxLength(1000)]],
@ -35,8 +37,9 @@ export class RevokeClaimDialogService {
this.revoke$
.pipe(
tap(() => this.error$.next({ hasError: false })),
switchMap((reason) =>
this.claimsApiService.revokeClaimByID(this.data.claimId, this.data.revision, reason).pipe(
withLatestFrom(this.contextService.organization$),
switchMap(([reason, org]) =>
this.claimsApiService.revokeClaimByID(org.id, this.data.claimId, this.data.revision, reason).pipe(
catchError((ex) => {
console.error(ex);
const error = { hasError: true, code: 'revokeClaimByIDFailed' };

View File

@ -1,15 +1,20 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { combineLatest } from 'rxjs';
import { pluck, switchMap } from 'rxjs/operators';
import { ClaimsService } from '@dsh/api/claims';
import { ContextService } from '@dsh/app/shared/services/context';
@Injectable()
export class RouteParamClaimService {
claim$ = this.route.params.pipe(
pluck('claimId'),
switchMap((id) => this.claimsService.getClaimByID(id))
claim$ = combineLatest([this.contextService.organization$, this.route.params.pipe(pluck('claimId'))]).pipe(
switchMap(([org, id]) => this.claimsService.getClaimByID(org.id, id))
);
constructor(private route: ActivatedRoute, private claimsService: ClaimsService) {}
constructor(
private route: ActivatedRoute,
private claimsService: ClaimsService,
private contextService: ContextService
) {}
}

View File

@ -1,12 +1,13 @@
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs';
import { catchError, filter, pluck, share, switchMap, tap } from 'rxjs/operators';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { catchError, filter, pluck, share, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { FileModification } from '@dsh/api-codegen/claim-management';
import { Conversation } from '@dsh/api-codegen/messages';
import { ClaimsService } from '@dsh/api/claims';
import { ContextService } from '@dsh/app/shared/services/context';
import { progress } from '../../../../custom-operators';
import { UiError } from '../../../ui-error';
@ -33,14 +34,15 @@ export class UpdateClaimService {
private routeParamClaimService: RouteParamClaimService,
private claimApiService: ClaimsService,
private snackBar: MatSnackBar,
private transloco: TranslocoService
private transloco: TranslocoService,
private contextService: ContextService
) {
const updated$ = this.updateBy$.pipe(
tap(() => this.error$.next({ hasError: false })),
toChangeset,
switchMap((changeset) => combineLatest([of(changeset), this.routeParamClaimService.claim$])),
switchMap(([changeset, { id, revision }]) =>
this.claimApiService.updateClaimByID(id, revision, changeset).pipe(
withLatestFrom(this.routeParamClaimService.claim$, this.contextService.organization$),
switchMap(([changeset, { id, revision }, org]) =>
this.claimApiService.updateClaimByID(org.id, id, revision, changeset).pipe(
catchError((ex) => {
console.error(ex);
const error = { hasError: true, code: 'updateClaimByIDFailed' };

View File

@ -3,10 +3,11 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { FetchResult, PartialFetcher } from '@rbkmoney/partial-fetcher';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
import { shareReplay, first, switchMap } from 'rxjs/operators';
import { Claim } from '@dsh/api-codegen/claim-management/swagger-codegen';
import { ClaimsService } from '@dsh/api/claims';
import { ContextService } from '@dsh/app/shared/services/context';
import { booleanDebounceTime, mapToTimestamp } from '@dsh/operators';
import { ClaimsSearchFiltersSearchParams } from '../../claims-search-filters/claims-search-filters-search-params';
@ -21,7 +22,8 @@ export class FetchClaimsService extends PartialFetcher<Claim, ClaimsSearchFilter
constructor(
private claimsService: ClaimsService,
private snackBar: MatSnackBar,
private transloco: TranslocoService
private transloco: TranslocoService,
private contextService: ContextService
) {
super();
this.errors$.subscribe(() => {
@ -34,11 +36,17 @@ export class FetchClaimsService extends PartialFetcher<Claim, ClaimsSearchFilter
params: ClaimsSearchFiltersSearchParams,
continuationToken: string
): Observable<FetchResult<Claim>> {
return this.claimsService.searchClaims(
this.searchLimit,
params.claimStatuses,
params.claimID,
continuationToken
return this.contextService.organization$.pipe(
first(),
switchMap(({ id }) =>
this.claimsService.searchClaims(
id,
this.searchLimit,
params.claimStatuses,
params.claimID,
continuationToken
)
)
);
}
}

View File

@ -7,8 +7,8 @@ import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { IdGeneratorService } from '@rbkmoney/id-generator';
import isNil from 'lodash-es/isNil';
import { combineLatest, Observable, of, Subject, throwError } from 'rxjs';
import { catchError, filter, map, mapTo, pluck, switchMap } from 'rxjs/operators';
import { combineLatest, Observable, of, Subject, throwError, EMPTY } from 'rxjs';
import { catchError, filter, map, mapTo, pluck, switchMap, withLatestFrom } from 'rxjs/operators';
import { OrgType, PartyContent, ReqResponse } from '@dsh/api-codegen/aggr-proxy';
import { Claim } from '@dsh/api-codegen/claim-management';
@ -21,6 +21,7 @@ import {
} from '@dsh/api/claims';
import { KonturFocusService } from '@dsh/api/kontur-focus';
import { QuestionaryService } from '@dsh/api/questionary';
import { ContextService } from '@dsh/app/shared/services/context';
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
import { shareReplayRefCount } from '@dsh/operators';
@ -39,11 +40,11 @@ export class CompanySearchService {
switchMap(({ claimID }) => of<number>(isNil(claimID) ? null : Number(claimID))),
shareReplayRefCount()
);
private claim$ = this.claimID$.pipe(
switchMap((claimID) =>
private claim$ = combineLatest([this.contextService.organization$, this.claimID$]).pipe(
switchMap(([org, claimID]) =>
isNil(claimID)
? of<Claim>(null)
: this.claimsService.getClaimByID(claimID).pipe(catchError(() => of<Claim>(null)))
: this.claimsService.getClaimByID(org.id, claimID).pipe(catchError(() => of<Claim>(null)))
),
shareReplayRefCount()
);
@ -59,7 +60,8 @@ export class CompanySearchService {
private konturFocusService: KonturFocusService,
private keycloakService: KeycloakService,
private idGenerator: IdGeneratorService,
private route: ActivatedRoute
private route: ActivatedRoute,
private contextService: ContextService
) {
this.leaveOnboarding$
.pipe(
@ -69,19 +71,23 @@ export class CompanySearchService {
)
.subscribe(() => void this.router.navigate(['/']));
combineLatest(this.claim$, this.claimID$)
.pipe(untilDestroyed(this))
.subscribe(([claim, claimID]) => {
if (
(claimID && !claim) ||
(claim &&
!claim.changeset.every(
(c) =>
isClaimModification(c.modification) &&
isExternalInfoModificationUnit(c.modification.claimModificationType)
))
)
void this.router.navigate(['./onboarding']);
});
.pipe(
switchMap(([claim, claimID]) => {
if (
(claimID && !claim) ||
(claim &&
!claim.changeset.every(
(c) =>
isClaimModification(c.modification) &&
isExternalInfoModificationUnit(c.modification.claimModificationType)
))
)
return this.contextService.navigate(['onboarding']);
return EMPTY;
}),
untilDestroyed(this)
)
.subscribe();
}
isKnownOrgType({ orgType }: PartyContent): boolean {
@ -94,11 +100,14 @@ export class CompanySearchService {
const defaultEmail = this.keycloakService.getUsername();
const questionaryData: QuestionaryData = { ...data, contactInfo: { email: defaultEmail, ...data.contactInfo } };
return this.claim$.pipe(
switchMap((claim) =>
withLatestFrom(this.contextService.organization$),
switchMap(([claim, org]) =>
claim
? this.claimsService.updateClaimByID(claim.id, claim.revision, changeset).pipe(mapTo(claim.id))
? this.claimsService
.updateClaimByID(org.id, claim.id, claim.revision, changeset)
.pipe(mapTo(claim.id))
: this.questionaryService.saveQuestionary(documentID, questionaryData).pipe(
switchMap(() => this.claimsService.createClaim(changeset)),
switchMap(() => this.claimsService.createClaim(org.id, changeset)),
pluck('id')
)
),

View File

@ -4,17 +4,22 @@ import { BehaviorSubject, combineLatest, defer } from 'rxjs';
import { shareReplay, switchMap } from 'rxjs/operators';
import { ClaimsService } from '@dsh/api/claims';
import { ContextService } from '@dsh/app/shared/services/context';
@Injectable()
export class ClaimService {
claim$ = combineLatest([this.route.params, defer(() => this.loadClaim$)]).pipe(
switchMap(([{ claimID }]) => this.claimsService.getClaimByID(claimID)),
claim$ = combineLatest([this.route.params, this.contextService.organization$, defer(() => this.loadClaim$)]).pipe(
switchMap(([{ claimID }, org]) => this.claimsService.getClaimByID(org.id, claimID)),
shareReplay(1)
);
private loadClaim$ = new BehaviorSubject<void>(undefined);
constructor(private route: ActivatedRoute, private claimsService: ClaimsService) {}
constructor(
private route: ActivatedRoute,
private claimsService: ClaimsService,
private contextService: ContextService
) {}
reloadClaim(): void {
this.loadClaim$.next();

View File

@ -9,6 +9,7 @@ import { map, pluck, share, switchMap, withLatestFrom } from 'rxjs/operators';
import { FileModification, FileModificationUnit } from '@dsh/api-codegen/claim-management';
import { QuestionaryData } from '@dsh/api-codegen/questionary';
import { ClaimsService, createFileModificationUnit, takeFileModificationUnits } from '@dsh/api/claims';
import { ContextService } from '@dsh/app/shared/services/context';
import { filterError, filterPayload, replaceError } from '@dsh/operators';
import { ClaimService } from '../../claim';
@ -36,14 +37,15 @@ export class UploadDocumentsService extends QuestionaryFormService {
private claimService: ClaimService,
private claimsService: ClaimsService,
private snackBar: MatSnackBar,
private transloco: TranslocoService
private transloco: TranslocoService,
private contextService: ContextService
) {
super(questionaryStateService, validityService, validationCheckService);
const uploadedFilesWithError$ = this.filesUploaded$.pipe(
map((fileIds) => fileIds.map((id) => createFileModificationUnit(id))),
withLatestFrom(this.claimService.claim$),
switchMap(([changeset, { id, revision }]) =>
this.claimsService.updateClaimByID(id, revision, changeset).pipe(replaceError)
withLatestFrom(this.claimService.claim$, this.contextService.organization$),
switchMap(([changeset, { id, revision }, org]) =>
this.claimsService.updateClaimByID(org.id, id, revision, changeset).pipe(replaceError)
),
share()
);
@ -51,9 +53,9 @@ export class UploadDocumentsService extends QuestionaryFormService {
map((unit) => [
createFileModificationUnit(unit.fileId, FileModification.FileModificationTypeEnum.FileDeleted),
]),
withLatestFrom(this.claimService.claim$),
switchMap(([changeset, { id, revision }]) =>
this.claimsService.updateClaimByID(id, revision, changeset).pipe(replaceError)
withLatestFrom(this.claimService.claim$, this.contextService.organization$),
switchMap(([changeset, { id, revision }, org]) =>
this.claimsService.updateClaimByID(org.id, id, revision, changeset).pipe(replaceError)
),
share()
);

View File

@ -3,9 +3,10 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { combineLatest, Observable, Subject } from 'rxjs';
import { filter, map, pluck, shareReplay, switchMap, switchMapTo } from 'rxjs/operators';
import { filter, map, pluck, shareReplay, switchMap, switchMapTo, withLatestFrom } from 'rxjs/operators';
import { ClaimsService } from '@dsh/api/claims';
import { ContextService } from '@dsh/app/shared/services/context';
import { ConfirmActionDialogComponent } from '@dsh/components/popups';
import { QuestionaryStateService } from '../questionary-state.service';
@ -32,7 +33,8 @@ export class StepCardService {
private claimsService: ClaimsService,
private dialog: MatDialog,
private route: ActivatedRoute,
private validityService: ValidityService
private validityService: ValidityService,
private contextService: ContextService
) {
const claimID$ = this.route.params.pipe(pluck('claimID'));
combineLatest([this.stepFlowService.stepFlow$, this.selectStepFlowIndex$])
@ -49,8 +51,13 @@ export class StepCardService {
switchMap(() => this.dialog.open(ConfirmActionDialogComponent).afterClosed()),
filter((r) => r === 'confirm'),
switchMapTo(claimID$),
switchMap((claimID) => this.claimsService.getClaimByID(claimID)),
switchMap(({ id, revision }) => this.claimsService.requestReviewClaimByID(id, revision)),
withLatestFrom(this.contextService.organization$),
switchMap(([claimID, org]) =>
this.claimsService.getClaimByID(org.id, claimID).pipe(map((claim) => [claim, org.id] as const))
),
switchMap(([{ id, revision }, orgId]) =>
this.claimsService.requestReviewClaimByID(orgId, id, revision)
),
switchMapTo(claimID$),
untilDestroyed(this)
)

View File

@ -1,2 +1 @@
export * from './sections.module';
export * from './sections.component';

View File

@ -2,10 +2,11 @@ import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { combineLatest, Observable } from 'rxjs';
import { pluck, shareReplay } from 'rxjs/operators';
import { pluck, shareReplay, switchMap } from 'rxjs/operators';
import { ClaimsService, CLAIM_STATUS } from '@dsh/api/claims';
import { ApiShopsService } from '@dsh/api/shop';
import { ContextService } from '@dsh/app/shared/services/context';
import { booleanDelay, takeError } from '@dsh/operators';
import { ActionBtnContent, TestEnvBtnContent } from './content-config';
@ -22,11 +23,19 @@ export class PaymentsService {
private shopService: ApiShopsService,
private claimService: ClaimsService,
private snackBar: MatSnackBar,
private transloco: TranslocoService
private transloco: TranslocoService,
private contextService: ContextService
) {
const claims = this.claimService
.search1000Claims([CLAIM_STATUS.Pending, CLAIM_STATUS.PendingAcceptance, CLAIM_STATUS.Review])
.pipe(shareReplay(1));
const claims = this.contextService.organization$.pipe(
switchMap(({ id }) =>
this.claimService.search1000Claims(id, [
CLAIM_STATUS.Pending,
CLAIM_STATUS.PendingAcceptance,
CLAIM_STATUS.Review,
])
),
shareReplay(1)
);
const contentConfig = toContentConf(this.shopService.shops$, claims);
this.actionBtnContent$ = contentConfig.pipe(pluck('actionBtnContent'));
this.testEnvBtnContent$ = contentConfig.pipe(pluck('testEnvBtnContent'));

View File

@ -1,26 +1,44 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SectionsComponent } from './sections.component';
const ROUTES: Routes = [
{
path: 'organization',
pathMatch: 'full',
redirectTo: 'organization/',
},
{
path: '',
loadChildren: () => import('./landing').then((m) => m.LandingModule),
pathMatch: 'full',
redirectTo: 'organization/',
},
{
path: 'claim-section',
loadChildren: () => import('./claim-section').then((m) => m.ClaimSectionModule),
},
{
path: 'payment-section',
loadChildren: () => import('./payment-section').then((m) => m.PaymentSectionModule),
},
{
path: 'wallet-section',
loadChildren: () => import('./wallet-section').then((m) => m.WalletSectionModule),
},
{
path: 'organization-section',
loadChildren: () => import('./organization-section').then((m) => m.OrginizationSectionModule),
path: 'organization/:organizationId',
component: SectionsComponent,
children: [
{
path: '',
loadChildren: () => import('./landing').then((m) => m.LandingModule),
},
{
path: 'claim-section',
loadChildren: () => import('./claim-section').then((m) => m.ClaimSectionModule),
},
{
path: 'payment-section',
loadChildren: () => import('./payment-section').then((m) => m.PaymentSectionModule),
},
{
path: 'wallet-section',
loadChildren: () => import('./wallet-section').then((m) => m.WalletSectionModule),
},
{
path: 'organization-section',
loadChildren: () => import('./organization-section').then((m) => m.OrginizationSectionModule),
},
],
},
{
path: '**',

View File

@ -0,0 +1,4 @@
<dsh-home>
<router-outlet *ngIf="bootstrapped$ | async"></router-outlet>
</dsh-home>
<dsh-feedback fxHide.lt-md></dsh-feedback>

View File

@ -1,7 +1,32 @@
import { Component } from '@angular/core';
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { untilDestroyed, UntilDestroy } from '@ngneat/until-destroy';
import * as Sentry from '@sentry/angular';
import { first } from 'rxjs/operators';
import { KeycloakTokenInfoService } from '@dsh/app/shared';
import { ContextService } from '@dsh/app/shared/services/context';
import { BootstrapService } from '../bootstrap.service';
@UntilDestroy()
@Component({
selector: 'dsh-sections',
template: `<router-outlet></router-outlet>`,
templateUrl: 'sections.component.html',
providers: [ContextService, BootstrapService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SectionsComponent {}
export class SectionsComponent implements OnInit {
bootstrapped$ = this.bootstrapService.bootstrapped$;
constructor(
private bootstrapService: BootstrapService,
private keycloakTokenInfoService: KeycloakTokenInfoService
) {}
ngOnInit(): void {
this.bootstrapService.bootstrap();
this.keycloakTokenInfoService.partyID$
.pipe(first(), untilDestroyed(this))
.subscribe((partyID) => Sentry.setUser({ id: partyID }));
}
}

View File

@ -1,10 +1,14 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ExtendedModule } from '@angular/flex-layout';
import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog';
import { DEBOUNCE_FETCHER_ACTION_TIME, DEFAULT_FETCHER_DEBOUNCE_ACTION_TIME } from '@rbkmoney/partial-fetcher';
import { ShopModule } from '@dsh/api/shop';
import { WalletModule } from '@dsh/api/wallet';
import { FeedbackModule } from '../feedback';
import { HomeModule } from '../home';
import { CHARTS_THEME } from './payment-section/analytics/charts-theme';
import { SectionsRoutingModule } from './sections-routing.module';
import { SectionsComponent } from './sections.component';
@ -17,9 +21,15 @@ import {
} from './tokens';
@NgModule({
imports: [SectionsRoutingModule, ShopModule, WalletModule],
declarations: [SectionsComponent],
exports: [SectionsComponent],
imports: [
CommonModule,
ShopModule,
WalletModule,
SectionsRoutingModule,
FeedbackModule,
HomeModule,
ExtendedModule,
],
providers: [
{ provide: SEARCH_LIMIT, useValue: DEFAULT_SEARCH_LIMIT },
{ provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: DEFAULT_DIALOG_CONFIG.medium },
@ -27,5 +37,7 @@ import {
{ provide: DEBOUNCE_FETCHER_ACTION_TIME, useValue: DEFAULT_FETCHER_DEBOUNCE_ACTION_TIME },
{ provide: CHARTS_THEME, useValue: DEFAULT_CHARTS_THEME },
],
declarations: [SectionsComponent],
exports: [SectionsRoutingModule],
})
export class SectionsModule {}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { IdGeneratorService } from '@rbkmoney/id-generator';
import { Observable } from 'rxjs';
import { mapTo, switchMap } from 'rxjs/operators';
import { mapTo, switchMap, first, map } from 'rxjs/operators';
import { Claim, Modification } from '@dsh/api-codegen/claim-management';
import { ClaimsService } from '@dsh/api/claims';
@ -13,6 +13,7 @@ import {
} from '@dsh/api/claims/claim-party-modification';
import { createInternationalContractPayoutToolModification } from '@dsh/api/claims/claim-party-modification/claim-contract-modification/create-international-contract-payout-tool-modification';
import { ContextService } from '../../../../../services/context';
import { InternationalShopEntityFormValue } from '../../types/international-shop-entity-form-value';
import {
payoutToolDetailsInternationalBankAccountToInternationalBankAccount,
@ -21,16 +22,24 @@ import {
@Injectable()
export class CreateInternationalShopEntityService {
constructor(private claimsService: ClaimsService, private idGenerator: IdGeneratorService) {}
constructor(
private claimsService: ClaimsService,
private idGenerator: IdGeneratorService,
private contextService: ContextService
) {}
createShop(creationData: InternationalShopEntityFormValue): Observable<Claim> {
return this.claimsService
.createClaim(this.createClaimsModifications(creationData))
.pipe(
switchMap((claim) =>
this.claimsService.requestReviewClaimByID(claim.id, claim.revision).pipe(mapTo(claim))
)
);
return this.contextService.organization$.pipe(
first(),
switchMap((org) =>
this.claimsService
.createClaim(org.id, this.createClaimsModifications(creationData))
.pipe(map((claim) => [claim, org.id] as const))
),
switchMap(([claim, orgId]) =>
this.claimsService.requestReviewClaimByID(orgId, claim.id, claim.revision).pipe(mapTo(claim))
)
);
}
private createClaimsModifications({

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { IdGeneratorService } from '@rbkmoney/id-generator';
import { forkJoin, Observable, of } from 'rxjs';
import { pluck, switchMap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { switchMap, first, map, mapTo } from 'rxjs/operators';
import { Claim, PartyModification } from '@dsh/api-codegen/claim-management';
import { ClaimsService } from '@dsh/api/claims';
@ -14,18 +14,28 @@ import {
makeShopLocation,
} from '@dsh/api/claims/claim-party-modification';
import { ContextService } from '../../../../../services/context';
import { RussianShopForm } from '../../types/russian-shop-entity';
@Injectable()
export class CreateRussianShopEntityService {
constructor(private claimsService: ClaimsService, private idGenerator: IdGeneratorService) {}
constructor(
private claimsService: ClaimsService,
private idGenerator: IdGeneratorService,
private contextService: ContextService
) {}
createShop(creationData: RussianShopForm): Observable<Claim> {
return this.claimsService.createClaim(this.createShopCreationModifications(creationData)).pipe(
switchMap((claim) => {
return forkJoin([of(claim), this.claimsService.requestReviewClaimByID(claim.id, claim.revision)]);
}),
pluck(0)
return this.contextService.organization$.pipe(
first(),
switchMap((org) =>
this.claimsService
.createClaim(org.id, this.createShopCreationModifications(creationData))
.pipe(map((claim) => [claim, org.id] as const))
),
switchMap(([claim, orgId]) =>
this.claimsService.requestReviewClaimByID(orgId, claim.id, claim.revision).pipe(mapTo(claim))
)
);
}

View File

@ -1,11 +0,0 @@
import { NgModule } from '@angular/core';
import { ContextService } from './context.service';
@NgModule({
imports: [],
declarations: [],
exports: [],
providers: [ContextService],
})
export class ContextModule {}

View File

@ -1,44 +1,76 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, defer, concat, EMPTY, ReplaySubject } from 'rxjs';
import { switchMap, pluck, tap, mapTo, catchError, shareReplay, distinctUntilChanged } from 'rxjs/operators';
import { ActivatedRoute, Router, NavigationExtras } from '@angular/router';
import { Observable, EMPTY, ReplaySubject, merge } from 'rxjs';
import { switchMap, pluck, tap, catchError, startWith, distinctUntilChanged, shareReplay, first } from 'rxjs/operators';
import { OrganizationsService } from '@dsh/api';
import { Organization } from '@dsh/api-codegen/organizations';
const ORGANIZATION_REG_EXP = /^(\/organization\/)([\w-]+)(.*)/;
@Injectable()
export class ContextService {
organization$: Observable<Organization> = concat(
this.organizationsService.getContext().pipe(
pluck('organizationId'),
catchError((err) => {
if (err instanceof HttpErrorResponse && err.status === 404)
return this.initOrganization().pipe(switchMap(() => EMPTY));
console.error(err);
return EMPTY;
})
organization$: Observable<Organization> = this.route.params.pipe(
startWith(this.route.snapshot.params),
pluck('organizationId'),
distinctUntilChanged(),
switchMap((id) =>
this.organizationsService.getOrg(id).pipe(
catchError((err) => {
console.error(err);
return this.getContextOrganization();
})
)
),
defer(() => this.switchOrganization$).pipe(
distinctUntilChanged(),
switchMap((id) => this.organizationsService.switchContext(id).pipe(mapTo(id)))
)
).pipe(
switchMap((id) => this.organizationsService.getOrg(id)),
shareReplay(1)
);
private switchOrganization$ = new ReplaySubject<string>(1);
constructor(private organizationsService: OrganizationsService) {}
constructor(
private organizationsService: OrganizationsService,
private route: ActivatedRoute,
private router: Router
) {
merge(this.switchOrganization$, this.organization$.pipe(pluck('id'))).subscribe(
(id) =>
void this.router.navigateByUrl(
this.router.url.replace(ORGANIZATION_REG_EXP, (match, start, oldId, end) =>
[start, id, end].join('')
)
)
);
}
switchOrganization(organizationId: string): void {
this.switchOrganization$.next(organizationId);
}
private initOrganization() {
return this.organizationsService.listOrgMembership(1).pipe(
pluck('result', 0, 'id'),
tap((id) => this.switchOrganization(id))
navigate(commands: (string | number)[], extras?: NavigationExtras): Observable<boolean> {
return this.organization$.pipe(
first(),
switchMap(({ id }) => this.router.navigate(['organization', id, ...commands], extras))
);
}
private getInitOrganization() {
return this.organizationsService.listOrgMembership(1).pipe(pluck('result', 0));
}
private getContextOrganization() {
return this.organizationsService.getContext().pipe(
pluck('organizationId'),
catchError((err) => {
if (err instanceof HttpErrorResponse && err.status === 404)
return this.getInitOrganization().pipe(
pluck('id'),
tap((id) => this.switchOrganization(id))
);
console.error(err);
return EMPTY;
}),
switchMap((id) => this.organizationsService.getOrg(id))
);
}
}

View File

@ -1,2 +1 @@
export * from './context.service';
export * from './context.module';