Skip to content

Commit

Permalink
feat: add and edit B2B users in organization-management (#260, #310)
Browse files Browse the repository at this point in the history
Closes #260

Co-authored-by: Silke Grüber <S.Grueber@intershop.de>
Co-authored-by: Danilo Hoffmann <d.hoffmann@intershop.de>
  • Loading branch information
3 people committed Jul 3, 2020
1 parent 5381f78 commit 3185084
Show file tree
Hide file tree
Showing 35 changed files with 1,211 additions and 121 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { HeaderModule } from '../header.module';

export class UsersDetailPage {
readonly tag = 'ish-users-detail-page';
readonly tag = 'ish-user-detail-page';

readonly header = new HeaderModule();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('User Management', () => {
page.goToUserDetailLink(_.selectedUser.email);
});
at(UsersDetailPage, page => {
page.name.should('have.text', `${_.selectedUser.name}`);
page.name.should('contain', `${_.selectedUser.name}`);
page.email.should('have.text', `${_.selectedUser.email}`);
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<div>
<div *ngIf="error" role="alert" class="alert alert-danger">
<span>{{ error.headers['error-key'] | translate }}</span>
</div>
<p class="indicates-required"><span class="required">*</span>{{ 'account.required_field.message' | translate }}</p>
<fieldset>
<ish-select-title [form]="form" controlName="title" [titles]="titles"></ish-select-title>
<ish-input
[form]="form"
controlName="firstName"
label="account.address.firstname.label"
[errorMessages]="{
required: 'account.user.new.firstname.error.required'
}"
></ish-input>
<ish-input
[form]="form"
controlName="lastName"
label="account.address.lastname.label"
[errorMessages]="{
required: 'account.user.new.lastname.error.required'
}"
></ish-input>
</fieldset>
<fieldset *ngIf="form.value.email !== undefined">
<ish-input
[form]="form"
controlName="email"
label="account.user.email.label"
[errorMessages]="{
required: 'account.update_email.email.error.notempty',
email: 'account.update_email.email.error.email'
}"
></ish-input>
</fieldset>
<fieldset>
<ish-input [form]="form" controlName="phone" label="account.profile.phone.label"></ish-input>
</fieldset>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { CustomValidators } from 'ngx-custom-validators';
import { of } from 'rxjs';
import { instance, mock, when } from 'ts-mockito';

import { AppFacade } from 'ish-core/facades/app.facade';
import { Locale } from 'ish-core/models/locale/locale.model';
import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component';
import { InputComponent } from 'ish-shared/forms/components/input/input.component';
import { SelectTitleComponent } from 'ish-shared/forms/components/select-title/select-title.component';

import { UserProfileFormComponent } from './user-profile-form.component';

describe('User Profile Form Component', () => {
let component: UserProfileFormComponent;
let fixture: ComponentFixture<UserProfileFormComponent>;
let element: HTMLElement;
let fb: FormBuilder;
let appFacade: AppFacade;

beforeEach(async(() => {
appFacade = mock(AppFacade);

TestBed.configureTestingModule({
imports: [ReactiveFormsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [
MockComponent(ErrorMessageComponent),
MockComponent(InputComponent),
MockComponent(SelectTitleComponent),
UserProfileFormComponent,
],
providers: [{ provide: AppFacade, useFactory: () => instance(appFacade) }],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(UserProfileFormComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
fb = TestBed.inject(FormBuilder);
when(appFacade.currentLocale$).thenReturn(of({ lang: 'en_US' } as Locale));

component.form = fb.group({
email: ['', [Validators.required, CustomValidators.email]],
});
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should display form input fields on creation', () => {
fixture.detectChanges();

expect(element.querySelector('[controlname=firstName]')).toBeTruthy();
expect(element.querySelector('[controlname=lastName]')).toBeTruthy();
expect(element.querySelector('[controlname=phone]')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { AppFacade } from 'ish-core/facades/app.facade';
import { HttpError } from 'ish-core/models/http-error/http-error.model';
import { Locale } from 'ish-core/models/locale/locale.model';
import { whenTruthy } from 'ish-core/utils/operators';
import { determineSalutations } from 'ish-shared/forms/utils/form-utils';

@Component({
selector: 'ish-user-profile-form',
templateUrl: './user-profile-form.component.html',
changeDetection: ChangeDetectionStrategy.Default,
})
export class UserProfileFormComponent implements OnInit, OnDestroy {
@Input() form: FormGroup;
@Input() error: HttpError;

currentLocale$: Observable<Locale>;
private destroy$ = new Subject();

titles = [];

constructor(private appFacade: AppFacade) {}

ngOnInit() {
this.currentLocale$ = this.appFacade.currentLocale$;

// determine default language from session and available locales
this.currentLocale$.pipe(whenTruthy(), takeUntil(this.destroy$)).subscribe(locale => {
this.titles = locale?.lang ? determineSalutations(locale.lang.slice(3)) : undefined;
});
}

ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
2 changes: 2 additions & 0 deletions projects/organization-management/src/app/exports/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// tslint:disable: no-barrel-files

export { OrganizationManagementModule } from '../organization-management.module';

export { OrganizationManagementBreadcrumbService } from '../services/organization-management-breadcrumb/organization-management-breadcrumb.service';
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
import { Injectable } from '@angular/core';
import { Store, select } from '@ngrx/store';

import { getSelectedUser, getUsers, getUsersError, getUsersLoading, loadUsers } from '../store/users';
import { B2bUser } from '../models/b2b-user/b2b-user.model';
import {
addUser,
getSelectedUser,
getUsers,
getUsersError,
getUsersLoading,
loadUsers,
updateUser,
} from '../store/users';

// tslint:disable:member-ordering
@Injectable({ providedIn: 'root' })
export class OrganizationManagementFacade {
constructor(private store: Store) {}

usersError$ = this.store.pipe(select(getUsersError));
usersLoading$ = this.store.pipe(select(getUsersLoading));
selectedUser$ = this.store.pipe(select(getSelectedUser));

users$() {
this.store.dispatch(loadUsers());
return this.store.pipe(select(getUsers));
}
usersError$ = this.store.pipe(select(getUsersError));
usersLoading$ = this.store.pipe(select(getUsersLoading));
selectedUser$ = this.store.pipe(select(getSelectedUser));

addUser(user: B2bUser) {
this.store.dispatch(
addUser({
user,
})
);
}

updateUser(user: B2bUser) {
this.store.dispatch(
updateUser({
user,
})
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Customer } from 'ish-core/models/customer/customer.model';
import { User } from 'ish-core/models/user/user.model';

export interface CustomerB2bUserType {
customer: Customer;
user: B2bUser;
}

export interface B2bUser extends Partial<User> {
name?: string; // list call only
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import { NgModule } from '@angular/core';

import { SharedModule } from 'ish-shared/shared.module';

import { UserProfileFormComponent } from './components/user/user-profile-form/user-profile-form.component';
import { OrganizationManagementRoutingModule } from './pages/organization-management-routing.module';
import { UsersDetailPageComponent } from './pages/users-detail/users-detail-page.component';
import { UserCreatePageComponent } from './pages/user-create/user-create-page.component';
import { UserDetailPageComponent } from './pages/user-detail/user-detail-page.component';
import { UserEditProfilePageComponent } from './pages/user-edit-profile/user-edit-profile-page.component';
import { UsersPageComponent } from './pages/users/users-page.component';
import { OrganizationManagementStoreModule } from './store/organization-management-store.module';

@NgModule({
declarations: [UsersDetailPageComponent, UsersPageComponent],
declarations: [
UserCreatePageComponent,
UserDetailPageComponent,
UserEditProfilePageComponent,
UserProfileFormComponent,
UsersPageComponent,
],
imports: [OrganizationManagementRoutingModule, OrganizationManagementStoreModule, SharedModule],
})
export class OrganizationManagementModule {}
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { UsersDetailPageComponent } from './users-detail/users-detail-page.component';
import { UserCreatePageComponent } from './user-create/user-create-page.component';
import { UserDetailPageComponent } from './user-detail/user-detail-page.component';
import { UserEditProfilePageComponent } from './user-edit-profile/user-edit-profile-page.component';
import { UsersPageComponent } from './users/users-page.component';

const routes: Routes = [
/**
* routes for the organization management
*
* visible for testing
*/
export const routes: Routes = [
{ path: '', redirectTo: 'users', pathMatch: 'full' },
{
path: 'users',
component: UsersPageComponent,
data: { breadcrumbData: [{ key: 'account.organization.user_management' }] },
},
{
path: 'users/create',
component: UserCreatePageComponent,
},
{
path: 'users/:B2BCustomerLogin',
component: UsersDetailPageComponent,
data: { breadcrumbData: [{ key: 'account.organization.user_management.user_detail' }] },
component: UserDetailPageComponent,
},
{
path: 'users/:B2BCustomerLogin/profile',
component: UserEditProfilePageComponent,
},
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<div class="row">
<div class="col-md-12">
<h1>{{ 'account.user.new.heading' | translate }}</h1>
<form *ngIf="form" [formGroup]="form" (ngSubmit)="submitForm()" class="form-horizontal" novalidate="novalidate">
<ish-user-profile-form [form]="profile" [error]="userError$ | async"></ish-user-profile-form>
<div class="row">
<div class="offset-md-4 col-md-8">
<button type="submit" class="btn btn-primary" [disabled]="formDisabled">
{{ 'account.user.new.button.create.label' | translate }}
</button>
<a routerLink="../../" class="btn btn-secondary">{{ 'account.cancel.link' | translate }}</a>
</div>
</div>
</form>
</div>
<ish-loading *ngIf="loading$ | async"></ish-loading>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';
import { CustomValidators } from 'ngx-custom-validators';
import { instance, mock } from 'ts-mockito';

import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component';

import { UserProfileFormComponent } from '../../components/user/user-profile-form/user-profile-form.component';
import { OrganizationManagementFacade } from '../../facades/organization-management.facade';

import { UserCreatePageComponent } from './user-create-page.component';

describe('User Create Page Component', () => {
let component: UserCreatePageComponent;
let fixture: ComponentFixture<UserCreatePageComponent>;
let element: HTMLElement;
let organizationManagementFacade: OrganizationManagementFacade;
let fb: FormBuilder;

beforeEach(async(() => {
organizationManagementFacade = mock(OrganizationManagementFacade);
TestBed.configureTestingModule({
imports: [ReactiveFormsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [MockComponent(LoadingComponent), MockComponent(UserProfileFormComponent), UserCreatePageComponent],
providers: [{ provide: OrganizationManagementFacade, useFactory: () => instance(organizationManagementFacade) }],
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(UserCreatePageComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
fb = TestBed.inject(FormBuilder);
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});

it('should submit a valid form when the user fills all required fields', () => {
fixture.detectChanges();

component.form = fb.group({
profile: fb.group({
firstName: ['Bernhard', [Validators.required]],
lastName: ['Boldner', [Validators.required]],
email: ['test@gmail.com', [Validators.required, CustomValidators.email]],
preferredLanguage: ['en_US', [Validators.required]],
}),
});

expect(component.formDisabled).toBeFalse();
component.submitForm();
expect(component.formDisabled).toBeFalse();
});

it('should disable submit button when the user submits an invalid form', () => {
fixture.detectChanges();

expect(component.formDisabled).toBeFalse();
component.submitForm();
expect(component.formDisabled).toBeTrue();
});
});
Loading

0 comments on commit 3185084

Please sign in to comment.