Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ClientApp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ npm-debug.log
yarn-error.log
testem.log
/typings
.angular/cache

# System Files
.DS_Store
Expand Down
7 changes: 6 additions & 1 deletion ClientApp/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';

Expand All @@ -10,6 +10,7 @@ import { HomeComponent } from './components/home/home.component';
import { CounterComponent } from './components/counter/counter.component';
import { SanctionedEntitiesComponent } from './components/sanctioned-entities/sanctioned-entities.component';
import { JumbotronCounterComponent } from './components/jumbotron-counter/jumbotron-counter.component';
import { SanctionedEntityAddComponent } from './components/sanctioned-entity-add/sanctioned-entity-add.component';


@NgModule({
Expand All @@ -19,16 +20,20 @@ import { JumbotronCounterComponent } from './components/jumbotron-counter/jumbot
HomeComponent,
CounterComponent,
SanctionedEntitiesComponent,
SanctionedEntityAddComponent,
JumbotronCounterComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
HttpClientModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'counter', component: CounterComponent },
{ path: 'sanctioned-entities', component: SanctionedEntitiesComponent },
{ path: 'sanctioned-entities/add', component: SanctionedEntityAddComponent },
{ path: '**', redirectTo: '' } // Wildcard route for a 404 page can be added here
])
],
providers: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ <h1>Counter</h1>

<p>This is a simple example of an Angular component.</p>

<p aria-live="polite">Current count: <strong>{{ currentCount }}</strong></p>
<p aria-live="polite">Current count: <strong>{{ currentCount$ | async }}</strong></p>

<button class="btn btn-primary" (click)="incrementCounter()">Increment</button>
10 changes: 7 additions & 3 deletions ClientApp/src/app/components/counter/counter.component.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { CounterService } from 'src/app/services/counter.service';

@Component({
selector: 'app-counter-component',
templateUrl: './counter.component.html'
})
export class CounterComponent {
public currentCount = 0;
public currentCount$: Observable<number> = this.counterService.getCounter();

public incrementCounter() {
this.currentCount++;
constructor(private readonly counterService: CounterService) {}

public incrementCounter(): void {
this.counterService.increment();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="jumbotron text-center">
<div class="jumbotron text-center" aria-live="polite">
<p>Current count:</p>
<p><strong>Please include the counter here</strong></p>
<p><strong>{{ currentCount$ | async }}</strong></p>
</div>
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { CounterService } from 'src/app/services/counter.service';

@Component({
selector: 'app-jumbotron-counter',
templateUrl: './jumbotron-counter.component.html'
})
export class JumbotronCounterComponent {
public currentCount$: Observable<number> = this.counterService.getCounter();

constructor(private readonly counterService: CounterService) {}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
<h1 id="tableLabel">Sanctioned Entities</h1>

<p *ngIf="!entities"><em>Loading...</em></p>
<div class="d-flex justify-content-end mb-3">
<a routerLink="/sanctioned-entities/add" class="btn btn-success">
+ Add Entity
</a>
</div>

<table class='table table-striped' aria-labelledby="tableLabel" *ngIf="entities">
<ng-container *ngIf="entities$ | async as entities; else loading">
<table
class="table table-striped"
aria-labelledby="tableLabel"
*ngIf="entities$ | async as entities"
>
<thead>
<tr>
<tr>
<th>Name</th>
<th>Domicile</th>
<th>Status</th>
</tr>
</tr>
</thead>
<tbody>
<tr *ngFor="let entity of entities">
<tr *ngFor="let entity of entities">
<td>{{ entity.name }}</td>
<td>{{ entity.domicile }}</td>
<td *ngIf="entity.accepted" class="text-success">Accepted</td>
<td *ngIf="!entity.accepted" class="text-danger">Rejected</td>
</tr>
</tr>
</tbody>
</table>
</table>
</ng-container>

<ng-template #loading>
<p><em>Loading...</em></p>
</ng-template>
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { Component } from '@angular/core';
import { SanctionedEntity } from '../../models/sanctioned-entity';
import { SanctionedEntitiesService } from '../../services/sanctioned-entities.service';
import { Observable } from 'rxjs';

@Component({
selector: 'app-sanctioned-entities',
templateUrl: './sanctioned-entities.component.html'
templateUrl: './sanctioned-entities.component.html',
})
export class SanctionedEntitiesComponent {
public entities: SanctionedEntity[] = [];
public entities$: Observable<SanctionedEntity[]> =
this.entitiesService.getSanctionedEntities();

constructor(private entitiesService: SanctionedEntitiesService) {
entitiesService.getSanctionedEntities().subscribe(entities => {
this.entities = entities;
});
}
constructor(private entitiesService: SanctionedEntitiesService) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<form class="mb-4" [formGroup]="entityForm" (ngSubmit)="addEntity()">
<div class="mb-3">
<label class="form-label" for="name"> Name </label>
<input
class="form-control"
formControlName="name"
id="name"
type="text"
[class.is-invalid]="
(entityForm.controls.name.touched || formSubmitted) &&
entityForm.controls.name.invalid
"
/>
<div
class="invalid-feedback"
*ngIf="
(entityForm.controls.name.touched || formSubmitted) &&
entityForm.controls.name.hasError('required')
"
>
Name is required.
</div>
</div>

<div class="mb-3">
<label class="form-label" for="domicile">Domicile</label>
<input
class="form-control"
id="domicile"
formControlName="domicile"
type="text"
[class.is-invalid]="
(entityForm.controls.domicile.touched || formSubmitted) &&
entityForm.controls.domicile.invalid
"
/>
<div
class="invalid-feedback"
*ngIf="
(entityForm.controls.domicile.touched || formSubmitted) &&
entityForm.controls.domicile.hasError('required')
"
>
Domicile is required.
</div>
</div>

<div
class="text-danger mb-2"
*ngIf="
(entityForm.errors?.['duplicateEntity'] &&
(formSubmitted || entityForm.touched))
"
>
An entity with the same name and domicile already exists.
</div>

<div class="form-check form-switch mb-3">
<input
class="form-check-input"
formControlName="accepted"
id="accepted"
type="checkbox"
/>
<label class="form-check-label" for="accepted">Accepted</label>
</div>

<div *ngIf="formError" class="text-danger mb-2">{{ formError }}</div>

<div class="mt-3">
<button
class="btn btn-primary"
type="submit"
[disabled]="entityForm.invalid || isSaving"
>
<span
*ngIf="isSaving"
class="spinner-border spinner-border-sm me-2"
></span>
Save
</button>
<button
class="btn btn-secondary ms-2"
type="button"
routerLink="/sanctioned-entities"
>
Cancel
</button>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { of, throwError } from 'rxjs';

import { SanctionedEntityAddComponent } from './sanctioned-entity-add.component';
import { SanctionedEntitiesService } from '../../services/sanctioned-entities.service';

describe('SanctionedEntityAddComponent', () => {
let component: SanctionedEntityAddComponent;
let fixture: ComponentFixture<SanctionedEntityAddComponent>;
let mockService: any;
let mockRouter: any;

beforeEach(() => {
mockService = {
addSanctionedEntity: jasmine.createSpy('addSanctionedEntity'),
getSanctionedEntities: jasmine
.createSpy('getSanctionedEntities')
.and.returnValue(of([])),
};

mockRouter = {
navigate: jasmine.createSpy('navigate'),
};

TestBed.configureTestingModule({
imports: [ReactiveFormsModule],
declarations: [SanctionedEntityAddComponent],
providers: [
{ provide: SanctionedEntitiesService, useValue: mockService },
{ provide: Router, useValue: mockRouter },
],
}).compileComponents();

fixture = TestBed.createComponent(SanctionedEntityAddComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create the component', () => {
expect(component).toBeTruthy();
});

it('should mark form as submitted and not call service if invalid', () => {
component.addEntity();
expect(component.formSubmitted).toBeTrue();
expect(mockService.addSanctionedEntity).not.toHaveBeenCalled();
});

it('should call service and navigate on successful add', fakeAsync(() => {
const entity = { name: 'Test', domicile: 'Earth', accepted: true };
component.entityForm.setValue(entity);
mockService.addSanctionedEntity.and.returnValue(of({}));

component.addEntity();
tick(); // simulate async

expect(component.isSaving).toBeFalse();
expect(mockService.addSanctionedEntity).toHaveBeenCalledWith(entity);
expect(mockRouter.navigate).toHaveBeenCalledWith(['/sanctioned-entities']);
}));

it('should handle error if service fails', fakeAsync(() => {
const entity = { name: 'Test', domicile: 'Earth', accepted: true };
component.entityForm.setValue(entity);
const error = { message: 'Duplicate entity' };
mockService.addSanctionedEntity.and.returnValue(throwError(() => error));

component.addEntity();
tick(); // simulate async

expect(component.isSaving).toBeFalse();
expect(component.formError).toBe('Duplicate entity');
expect(mockRouter.navigate).not.toHaveBeenCalled();
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { SanctionedEntity } from '../../models/sanctioned-entity';
import { SanctionedEntitiesService } from '../../services/sanctioned-entities.service';
import { sanctionedEntityValidator } from 'src/app/validators/sanctioned-entity.validator';
import { take } from 'rxjs';
import { Router } from '@angular/router';

@Component({
selector: 'app-sanctioned-entity-add',
templateUrl: './sanctioned-entity-add.component.html',
})
export class SanctionedEntityAddComponent {
public entityForm: FormGroup;
public formSubmitted = false;
public formError = '';
public isSaving = false;

constructor(
private fb: FormBuilder,
private router: Router,
private readonly entitiesService: SanctionedEntitiesService
) {
this.entityForm = this.fb.group(
{
name: ['', Validators.required],
domicile: ['', Validators.required],
accepted: [false],
},
{
asyncValidators: [sanctionedEntityValidator(this.entitiesService)],
updateOn: 'blur',
}
);
}

public addEntity(): void {
this.formSubmitted = true;
this.formError = '';

if (this.entityForm.invalid) return;

this.isSaving = true;

const newEntity: SanctionedEntity = this.entityForm.value;

this.entitiesService
.addSanctionedEntity(newEntity)
.pipe(take(1))
.subscribe({
next: () => {
this.isSaving = false;
this.router.navigate(['/sanctioned-entities']);
},
error: (err) => {
this.isSaving = false;
this.formError = err.message || 'Failed to add entity';
},
});
}
}
Loading