feat: add edit page
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { Main } from './pages/main/main';
|
import { Main } from './pages/main/main';
|
||||||
|
import { Edit } from './pages/edit/edit';
|
||||||
|
import { contactResolver } from './pages/edit/contact-resolver';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
@@ -7,8 +9,13 @@ export const routes: Routes = [
|
|||||||
component: Main,
|
component: Main,
|
||||||
pathMatch: 'full',
|
pathMatch: 'full',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:id',
|
||||||
|
component: Edit,
|
||||||
|
resolve: { contact: contactResolver },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
redirectTo: '',
|
redirectTo: '',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,21 +7,25 @@ import { ContactService } from '../../pages/main/contact.service';
|
|||||||
import { ContactDTO } from '../../models/ContactDTO';
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
describe('ContactListTable', () => {
|
describe('ContactListTable', () => {
|
||||||
let component: ContactListTable;
|
let component: ContactListTable;
|
||||||
let fixture: ComponentFixture<ContactListTable>;
|
let fixture: ComponentFixture<ContactListTable>;
|
||||||
let contactService: jasmine.SpyObj<ContactService>;
|
let contactService: jasmine.SpyObj<ContactService>;
|
||||||
|
let router: jasmine.SpyObj<Router>;
|
||||||
let CONTACT_LIST_MOCK: ContactDTO[];
|
let CONTACT_LIST_MOCK: ContactDTO[];
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
contactService = jasmine.createSpyObj(ContactService.name, ['delete']);
|
contactService = jasmine.createSpyObj(ContactService.name, ['delete']);
|
||||||
|
router = jasmine.createSpyObj(Router.name, ['navigate']);
|
||||||
CONTACT_LIST_MOCK = [new ContactDTO(1, 'MOCK', 'MOCK', '5491122222222')];
|
CONTACT_LIST_MOCK = [new ContactDTO(1, 'MOCK', 'MOCK', '5491122222222')];
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ContactListTable],
|
imports: [ContactListTable],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: STRINGS_INJECTOR, useValue: strings },
|
{ provide: STRINGS_INJECTOR, useValue: strings },
|
||||||
{ provide: ContactService, useValue: contactService },
|
{ provide: ContactService, useValue: contactService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
contactService.delete.and.returnValue(of([]));
|
contactService.delete.and.returnValue(of([]));
|
||||||
@@ -36,7 +40,7 @@ describe('ContactListTable', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
it('should call for valid ID', () => {
|
it('should call delete for valid ID', () => {
|
||||||
const deleteSpy = spyOn(component, 'delete').and.callThrough();
|
const deleteSpy = spyOn(component, 'delete').and.callThrough();
|
||||||
const deleteButton = fixture.debugElement
|
const deleteButton = fixture.debugElement
|
||||||
.query(By.css('app-contact-actions-bar'))
|
.query(By.css('app-contact-actions-bar'))
|
||||||
@@ -55,4 +59,22 @@ describe('ContactListTable', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('edit', () => {
|
||||||
|
it("shouldn't navigate if ID is falsy", () => {
|
||||||
|
CONTACT_LIST_MOCK[0].id = undefined;
|
||||||
|
const editButton = fixture.debugElement
|
||||||
|
.query(By.css('app-contact-actions-bar'))
|
||||||
|
.query(By.css('.btn--edit'));
|
||||||
|
editButton.triggerEventHandler('click');
|
||||||
|
expect(router.navigate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call navigate for valid ID', () => {
|
||||||
|
const editButton = fixture.debugElement
|
||||||
|
.query(By.css('app-contact-actions-bar'))
|
||||||
|
.query(By.css('.btn--edit'));
|
||||||
|
editButton.triggerEventHandler('click');
|
||||||
|
expect(router.navigate).toHaveBeenCalledOnceWith(['edit', CONTACT_LIST_MOCK[0].id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Component, inject, input } from '@angular/core';
|
import { Component, inject, input } from '@angular/core';
|
||||||
import { STRINGS_INJECTOR } from '../../app.config';
|
import { STRINGS_INJECTOR } from '../../app.config';
|
||||||
import { ContactDTO } from '../../models/ContactDTO';
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
import { ContactActionsBar } from "../contact-actions-bar/contact-actions-bar";
|
import { ContactActionsBar } from '../contact-actions-bar/contact-actions-bar';
|
||||||
import { ContactService } from '../../pages/main/contact.service';
|
import { ContactService } from '../../pages/main/contact.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-contact-list-table',
|
selector: 'app-contact-list-table',
|
||||||
@@ -14,14 +15,15 @@ export class ContactListTable {
|
|||||||
contactList = input<ContactDTO[]>([]);
|
contactList = input<ContactDTO[]>([]);
|
||||||
protected readonly strings = inject(STRINGS_INJECTOR);
|
protected readonly strings = inject(STRINGS_INJECTOR);
|
||||||
private readonly contactService = inject(ContactService);
|
private readonly contactService = inject(ContactService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
edit(id?: number) {
|
edit(id?: number) {
|
||||||
if(!id) return;
|
if (!id) return;
|
||||||
|
this.router.navigate(['edit', id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id?: number) {
|
delete(id?: number) {
|
||||||
if(!id) return;
|
if (!id) return;
|
||||||
this.contactService.delete(id).subscribe();
|
this.contactService.delete(id).subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
88
src/app/pages/edit/contact-resolver.spec.ts
Normal file
88
src/app/pages/edit/contact-resolver.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRouteSnapshot, ResolveFn, Router } from '@angular/router';
|
||||||
|
import { contactResolver } from './contact-resolver';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
describe('contactResolver', () => {
|
||||||
|
let httpTestingController: HttpTestingController;
|
||||||
|
let router: jasmine.SpyObj<Router>;
|
||||||
|
|
||||||
|
let ROUTE_MOCK: jasmine.SpyObj<ActivatedRouteSnapshot>;
|
||||||
|
|
||||||
|
const executeResolver: ResolveFn<Observable<ContactDTO | null>> = (...resolverParameters) =>
|
||||||
|
TestBed.runInInjectionContext(() => contactResolver(...resolverParameters));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
router = jasmine.createSpyObj(Router.name, ['navigate']);
|
||||||
|
ROUTE_MOCK = jasmine.createSpyObj(ActivatedRouteSnapshot.name, [], {
|
||||||
|
paramMap: {
|
||||||
|
get() {
|
||||||
|
return '1';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
provideHttpClient(),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(executeResolver).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate back on error response', () => {
|
||||||
|
(
|
||||||
|
executeResolver(ROUTE_MOCK, {
|
||||||
|
url: '',
|
||||||
|
root: new ActivatedRouteSnapshot(),
|
||||||
|
}) as Observable<ContactDTO | null>
|
||||||
|
).subscribe();
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiUrl}/contacts/${ROUTE_MOCK.paramMap.get('id')}`
|
||||||
|
);
|
||||||
|
expect(req.request.method).toEqual('GET');
|
||||||
|
req.flush(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
code: 4400,
|
||||||
|
message: 'Resource not found',
|
||||||
|
details: 'Contact not found',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ status: 404, statusText: 'Resource not found' }
|
||||||
|
);
|
||||||
|
expect(router.navigate).toHaveBeenCalledOnceWith(['/']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return and observable with the fetched contact', () => {
|
||||||
|
(
|
||||||
|
executeResolver(ROUTE_MOCK, {
|
||||||
|
url: '',
|
||||||
|
root: new ActivatedRouteSnapshot(),
|
||||||
|
}) as Observable<ContactDTO | null>
|
||||||
|
).subscribe();
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiUrl}/contacts/${ROUTE_MOCK.paramMap.get('id')}`
|
||||||
|
);
|
||||||
|
expect(req.request.method).toEqual('GET');
|
||||||
|
req.flush({
|
||||||
|
data: new ContactDTO(1, 'mock', 'mock', 'mock'),
|
||||||
|
success: true,
|
||||||
|
message: 'Contact retrieved successfully',
|
||||||
|
});
|
||||||
|
httpTestingController.verify();
|
||||||
|
});
|
||||||
|
});
|
||||||
19
src/app/pages/edit/contact-resolver.ts
Normal file
19
src/app/pages/edit/contact-resolver.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { ResolveFn, Router } from '@angular/router';
|
||||||
|
import { ContactService } from '../main/contact.service';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { catchError, map, Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
export const contactResolver: ResolveFn<Observable<ContactDTO | null>> = (route, _) => {
|
||||||
|
const contactService = inject(ContactService);
|
||||||
|
const router = inject(Router);
|
||||||
|
const contactId = route.paramMap.get('id') ?? '0';
|
||||||
|
|
||||||
|
return contactService.findById(contactId).pipe(
|
||||||
|
map((response) => response.data),
|
||||||
|
catchError(() => {
|
||||||
|
router.navigate(['/']);
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
24
src/app/pages/edit/edit.html
Normal file
24
src/app/pages/edit/edit.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<app-main-header
|
||||||
|
[title]="strings.editContact"
|
||||||
|
>
|
||||||
|
<app-rounded-btn
|
||||||
|
(keydown)="goBack()"
|
||||||
|
(click)="goBack()"
|
||||||
|
slot="action-button"
|
||||||
|
text="Go back"
|
||||||
|
/>
|
||||||
|
</app-main-header>
|
||||||
|
<app-card bgColor="var(--secondary)">
|
||||||
|
@if(form$|async; as form){
|
||||||
|
<app-contact-form
|
||||||
|
class="form"
|
||||||
|
(contact)="edit(form.value)"
|
||||||
|
[form]="form"
|
||||||
|
[submitText]="strings.save"
|
||||||
|
><app-form-header
|
||||||
|
[title]="strings.editTheContact|upperfirst"
|
||||||
|
slot="header"
|
||||||
|
/>
|
||||||
|
</app-contact-form>
|
||||||
|
}
|
||||||
|
</app-card>
|
||||||
8
src/app/pages/edit/edit.scss
Normal file
8
src/app/pages/edit/edit.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
app-card {
|
||||||
|
margin-top: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.form {
|
||||||
|
display: block;
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
70
src/app/pages/edit/edit.spec.ts
Normal file
70
src/app/pages/edit/edit.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Edit } from './edit';
|
||||||
|
import { STRINGS_INJECTOR } from '../../app.config';
|
||||||
|
import { strings } from '../../strings';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ContactService } from '../main/contact.service';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('Edit', () => {
|
||||||
|
let component: Edit;
|
||||||
|
let fixture: ComponentFixture<Edit>;
|
||||||
|
|
||||||
|
let activatedRoute: jasmine.SpyObj<ActivatedRoute>;
|
||||||
|
let contactService: jasmine.SpyObj<ContactService>;
|
||||||
|
let router: jasmine.SpyObj<Router>;
|
||||||
|
|
||||||
|
let CONTACT_MOCK: ContactDTO;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
CONTACT_MOCK = new ContactDTO(1, 'mock', 'mock', 'mock');
|
||||||
|
activatedRoute = jasmine.createSpyObj(ActivatedRoute.name, [], {
|
||||||
|
data: of({ contact: CONTACT_MOCK }),
|
||||||
|
});
|
||||||
|
contactService = jasmine.createSpyObj(ContactService.name, ['update']);
|
||||||
|
router = jasmine.createSpyObj(Router.name, ['navigate']);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Edit],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
|
{ provide: ContactService, useValue: contactService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: STRINGS_INJECTOR, useValue: strings },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
TestBed.inject(ActivatedRoute);
|
||||||
|
fixture = TestBed.createComponent(Edit);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update on form submission', () => {
|
||||||
|
contactService.update.and.returnValue(
|
||||||
|
of({ success: true, data: null, message: 'Contact updated successfully' })
|
||||||
|
);
|
||||||
|
const MODIFICATION_MOCK = 'modified';
|
||||||
|
const nameInput = fixture.debugElement
|
||||||
|
.query(By.css('[formControlName="name"]'))
|
||||||
|
.query(By.css('input'));
|
||||||
|
nameInput.triggerEventHandler('input', { target: { value: MODIFICATION_MOCK } });
|
||||||
|
CONTACT_MOCK.name = MODIFICATION_MOCK;
|
||||||
|
const submitBtn = fixture.debugElement.query(By.css('app-squared-btn')).query(By.css('button'));
|
||||||
|
(<HTMLButtonElement>submitBtn.nativeElement).click();
|
||||||
|
expect(contactService.update).toHaveBeenCalledOnceWith(CONTACT_MOCK);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate back', () => {
|
||||||
|
const goBackBtn = fixture.debugElement.query(By.css('app-rounded-btn'));
|
||||||
|
goBackBtn.triggerEventHandler('click');
|
||||||
|
expect(router.navigate).toHaveBeenCalledOnceWith(['./..'], { relativeTo: activatedRoute });
|
||||||
|
});
|
||||||
|
});
|
||||||
56
src/app/pages/edit/edit.ts
Normal file
56
src/app/pages/edit/edit.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { MainHeader } from '../../components/main-header/main-header';
|
||||||
|
import { STRINGS_INJECTOR } from '../../app.config';
|
||||||
|
import { RoundedBtn } from '../../components/rounded-btn/rounded-btn';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Card } from '../../components/card/card';
|
||||||
|
import { ContactForm } from '../../components/contact-form/contact-form';
|
||||||
|
import { FormHeader } from '../../components/form-header/form-header';
|
||||||
|
import { UpperfirstPipe } from '../../pipes/upperfirst-pipe';
|
||||||
|
import { map, Observable, take, tap } from 'rxjs';
|
||||||
|
import { ContactDTO } from '../../models/ContactDTO';
|
||||||
|
import { FormGroupContact } from '../../utils/form-group-contact';
|
||||||
|
import { AsyncPipe } from '@angular/common';
|
||||||
|
import { ContactService } from '../main/contact.service';
|
||||||
|
import { ContactFormValue } from '../../types/ContactFormValue.type';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-edit',
|
||||||
|
imports: [MainHeader, RoundedBtn, Card, ContactForm, FormHeader, UpperfirstPipe, AsyncPipe],
|
||||||
|
templateUrl: './edit.html',
|
||||||
|
styleUrl: './edit.scss',
|
||||||
|
})
|
||||||
|
export class Edit {
|
||||||
|
protected readonly strings = inject(STRINGS_INJECTOR);
|
||||||
|
private readonly activatedRoute = inject(ActivatedRoute);
|
||||||
|
private readonly contactService = inject(ContactService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
protected contactId?: number;
|
||||||
|
protected readonly form$ = (<Observable<{ contact: ContactDTO }>>this.activatedRoute.data).pipe(
|
||||||
|
take(1),
|
||||||
|
map((data) => data.contact),
|
||||||
|
tap((contact) => (this.contactId = contact.id)),
|
||||||
|
map((contact) => new FormGroupContact(contact.name, contact.company, contact.phone))
|
||||||
|
);
|
||||||
|
|
||||||
|
edit(form: ContactFormValue) {
|
||||||
|
if (this.contactId) {
|
||||||
|
const contact = new ContactDTO(
|
||||||
|
this.contactId,
|
||||||
|
form.name ?? '',
|
||||||
|
form.company ?? '',
|
||||||
|
form.phone ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
this.contactService
|
||||||
|
.update(contact)
|
||||||
|
.pipe(tap(() => this.goBack()))
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goBack() {
|
||||||
|
this.router.navigate(['./..'], { relativeTo: this.activatedRoute });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user