FRONTEND-534: Accept invitation page (#442)

This commit is contained in:
Rinat Arsaev 2021-04-26 16:48:08 +03:00 committed by GitHub
parent c84efbedaf
commit 1dfcc0e45c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 177 additions and 48 deletions

View File

@ -0,0 +1,27 @@
<div
*transloco="let t; scope: 'organizations'; read: 'organizations.acceptInvitation'"
fxLayout="column"
fxLayoutGap="48px"
fxLayoutAlign=" center"
class="dsh-accept-invitation"
>
<dsh-spinner *ngIf="isLoading$ | async; else loaded"></dsh-spinner>
<ng-template #loaded>
<ng-container *ngIf="isCompleted; else confirm">
<ng-container *ngIf="hasError; else success">
<div class="dsh-headline">{{ t('error.title') }}</div>
</ng-container>
<ng-template #success>
<div class="dsh-headline">{{ t('success.title') }}</div>
<a dshLink routerLink="/organizations">{{ t('success.goToOrganizationsList') }}</a>
</ng-template>
</ng-container>
<ng-template #confirm>
<div class="dsh-headline">{{ t('confirm.title') }}</div>
<button dsh-button (click)="accept()" color="accent" size="lg" class="accept-button">
{{ t('confirm.button') }}
</button>
<div dshTextColor="secondary" class="dsh-caption">{{ t('confirm.refuseDescription') }}</div>
</ng-template>
</ng-template>
</div>

View File

@ -0,0 +1,7 @@
.dsh-accept-invitation {
margin-top: 260px;
}
.accept-button {
width: 180px;
}

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { anything, deepEqual, mock, verify, when } from 'ts-mockito';
@ -15,14 +15,11 @@ describe('AcceptInvitationComponent', () => {
let fixture: ComponentFixture<AcceptInvitationComponent>;
let component: AcceptInvitationComponent;
let mockRoute: ActivatedRoute;
let mockRouter: Router;
let mockOrganizationsService: OrganizationsService;
beforeEach(async () => {
mockRouter = mock(Router);
mockRoute = mock(ActivatedRoute);
when(mockRoute.snapshot).thenReturn({ params: { token: '123' } } as any);
when(mockRoute.params).thenReturn(of({ token: '123' } as any));
mockOrganizationsService = mock(OrganizationsService);
when(mockOrganizationsService.joinOrg(anything())).thenReturn(of({} as any));
@ -34,7 +31,6 @@ describe('AcceptInvitationComponent', () => {
provideMockService(OrganizationsService, mockOrganizationsService),
provideMockService(ErrorService),
provideMockService(ActivatedRoute, mockRoute),
provideMockService(Router, mockRouter),
],
}).compileComponents();
@ -48,9 +44,11 @@ describe('AcceptInvitationComponent', () => {
expect(component).toBeTruthy();
});
it('should be init', () => {
verify(mockOrganizationsService.joinOrg(deepEqual({ invitation: '123' }))).once();
verify(mockRouter.navigate(deepEqual(['organizations']))).once();
expect().nothing();
describe('accept method', () => {
it('should be join to org', () => {
component.accept();
verify(mockOrganizationsService.joinOrg(deepEqual({ invitation: '123' }))).once();
expect().nothing();
});
});
});

View File

@ -1,36 +1,46 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { BehaviorSubject, Subscription } from 'rxjs';
import { first, pluck, switchMap } from 'rxjs/operators';
import { OrganizationsService } from '@dsh/api';
import { ErrorService } from '@dsh/app/shared';
import { inProgressTo } from '@dsh/utils';
@UntilDestroy()
@Component({
selector: 'dsh-accept-invitation',
template: ``,
templateUrl: 'accept-invitation.component.html',
styleUrls: ['accept-invitation.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AcceptInvitationComponent implements OnInit {
export class AcceptInvitationComponent {
hasError = false;
isCompleted = false;
isLoading$ = new BehaviorSubject<boolean>(false);
constructor(
private route: ActivatedRoute,
private router: Router,
private organizationsService: OrganizationsService,
private errorService: ErrorService
) {}
ngOnInit() {
this.acceptInvitation();
}
private acceptInvitation() {
const invitation = this.route.snapshot.params.token;
this.organizationsService
.joinOrg({ invitation })
.pipe(untilDestroyed(this))
.subscribe(
() => this.router.navigate(['organizations']),
(err) => this.errorService.error(err)
);
@inProgressTo('isLoading$')
accept(): Subscription {
return this.route.params
.pipe(
first(),
pluck('token'),
switchMap((invitation: string) => this.organizationsService.joinOrg({ invitation })),
untilDestroyed(this)
)
.subscribe({
error: (err) => {
this.errorService.error(err, false);
this.hasError = true;
},
})
.add(() => (this.isCompleted = true));
}
}

View File

@ -1,13 +1,27 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FlexModule } from '@angular/flex-layout';
import { TranslocoModule } from '@ngneat/transloco';
import { ErrorModule } from '@dsh/app/shared';
import { ButtonModule } from '@dsh/components/buttons';
import { IndicatorsModule } from '@dsh/components/indicators';
import { LinkModule } from '@dsh/components/link';
import { AcceptInvitationRoutingModule } from './accept-invitation-routing.module';
import { AcceptInvitationComponent } from './accept-invitation.component';
@NgModule({
imports: [CommonModule, AcceptInvitationRoutingModule, ErrorModule],
imports: [
CommonModule,
AcceptInvitationRoutingModule,
ErrorModule,
TranslocoModule,
ButtonModule,
FlexModule,
IndicatorsModule,
LinkModule,
],
declarations: [AcceptInvitationComponent],
exports: [AcceptInvitationComponent],
})

View File

@ -1,27 +1,35 @@
import { HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { MatSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar';
import { TranslocoService } from '@ngneat/transloco';
import { ErrorResult } from '@dsh/app/shared/services/error/models/error-result';
import { NotificationService } from '../notification';
import { CommonError } from './models/common-error';
// TODO: collect error information
@Injectable()
export class ErrorService {
constructor(private notificationService: NotificationService, private transloco: TranslocoService) {}
error(error: Error): MatSnackBarRef<SimpleSnackBar> {
// TODO: collect and dev log error information
error(error: unknown, notify = true): ErrorResult {
const errorResult: ErrorResult = { error: this.parse(error) };
if (notify) {
errorResult.notification = this.notificationService.error(errorResult.error.message);
}
return errorResult;
}
private parse(error: unknown): CommonError {
if (error instanceof CommonError) {
return error;
}
if (error instanceof TypeError) {
return this.notificationService.error(this.transloco.translate('notification.error'));
return new CommonError(this.transloco.translate('notification.error'));
}
if (error instanceof HttpErrorResponse) {
return this.notificationService.error(this.transloco.translate('notification.httpError'));
return new CommonError(this.transloco.translate('notification.httpError'));
}
if (error instanceof CommonError) {
return this.notificationService.error(error.message);
}
return this.notificationService.error(this.transloco.translate('notification.error'));
return new CommonError(this.transloco.translate('notification.error'));
}
}

View File

@ -0,0 +1,8 @@
import { MatSnackBarRef, SimpleSnackBar } from '@angular/material/snack-bar';
import { CommonError } from '@dsh/app/shared';
export interface ErrorResult {
error: CommonError;
notification?: MatSnackBarRef<SimpleSnackBar>;
}

View File

@ -131,5 +131,19 @@
"editRolesDialog": {
"title": "Редактирование ролей",
"save": "Сохранить"
},
"acceptInvitation": {
"confirm": {
"title": "Подтвердите вступление в организацию",
"button": "Подтвердить",
"refuseDescription": "Для отказа просто покиньте эту страницу"
},
"success": {
"title": "Вы успешно вступили в организацию",
"goToOrganizationsList": "Перейти к списку организаций"
},
"error": {
"title": "Произошла ошибка"
}
}
}

View File

@ -1,18 +1,20 @@
import { Directive, HostBinding, Input } from '@angular/core';
import { Color } from '@dsh/components/indicators/text-color/types/color';
const PREFIX = 'dsh-text-color';
@Directive({
selector: '[dshTextColor]',
})
export class TextColorDirective {
@Input() dshTextColor: 'primary' | 'secondary';
@Input() dshTextColor: keyof Color | Color;
@HostBinding('attr.class') get class(): string {
const prefix = 'dsh-text-color';
switch (this.dshTextColor) {
case 'primary':
return `${prefix}-primary`;
case 'secondary':
return `${prefix}-secondary`;
}
@HostBinding(`class.${PREFIX}-${Color.Primary}`) get primaryColor() {
return this.dshTextColor === Color.Primary;
}
@HostBinding(`class.${PREFIX}-${Color.Secondary}`) get secondaryColor() {
return this.dshTextColor === Color.Secondary;
}
}

View File

@ -0,0 +1,4 @@
export enum Color {
Primary = 'primary',
Secondary = 'secondary',
}

View File

@ -0,0 +1,13 @@
@import '~@angular/material/theming';
@mixin dsh-link-theme($theme) {
.dsh-link {
color: map-get($theme, primary, default);
}
}
@mixin dsh-link-typography($config) {
.dsh-link {
@include mat-typography-level-to-styles($config, body-1);
}
}

View File

@ -0,0 +1,2 @@
export * from './link.directive';
export * from './link.module';

View File

@ -0,0 +1,8 @@
import { Directive, HostBinding } from '@angular/core';
@Directive({
selector: 'a[dshLink]',
})
export class LinkDirective {
@HostBinding(`class.dsh-link`) linkClass = true;
}

View File

@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';
import { LinkDirective } from './link.directive';
@NgModule({
declarations: [LinkDirective],
exports: [LinkDirective],
})
export class LinkModule {}

View File

@ -29,6 +29,7 @@
@import '../../components/nested-table/nested-table-theme';
@import '../../components/layout/headline/headline-theme';
@import '../../components/indicators/selection/selection-theme';
@import '../../components/link/link-theme';
@import '../../app/home/home-theme';
@import '../../app/home/actionbar/actionbar-theme';
@ -93,4 +94,5 @@
@include dsh-breadcrumb-theme($theme);
@include dsh-text-color-theme($theme);
@include dsh-selection-theme($theme);
@include dsh-link-theme($theme);
}

View File

@ -21,6 +21,7 @@
@import '../../components/indicators/last-updated/last-updated-theme';
@import '../../components/global-banner/global-banner-theme';
@import '../../components/navigation-link/navigation-link-theme';
@import '../../components/link/link-theme';
@import '../../app/home/home-theme';
@import '../../app/home/welcome-image/welcome-image-theme';
@ -55,4 +56,5 @@
@include dsh-link-label-typography($config);
@include dsh-navigation-link-typography($config);
@include dsh-breadcrumb-typography($config);
@include dsh-link-typography($config);
}

View File

@ -116,6 +116,7 @@
margin: 0;
}
.dsh-display-3,
#{$selector} .dsh-display-3 {
@include mat-typography-level-to-styles($config, display-3);
margin: 0;