Skip to content

Commit

Permalink
Merge pull request #554 from fecgov/feature/471-app-contact-lookup-pr…
Browse files Browse the repository at this point in the history
…ototype

Feature/471 app contact lookup prototype
  • Loading branch information
toddlees authored Sep 7, 2022
2 parents eb3fc4d + 1045f01 commit e54be5c
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,16 @@
pRipple
label="Delete"
icon="pi pi-trash"
class="p-button-danger"
class="p-button-danger mr-2"
(click)="deleteSelectedItems()"
[disabled]="!selectedItems.length"
></button>
<app-contact-lookup
[contactTypeOptions]="contactTypeOptions"
[maxFecResults]="5"
[maxFecfileResults]="5"
(contactSelect)="onContactLookupSelect($event)">
</app-contact-lookup>
</ng-template>

<ng-template pTemplate="right">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormBuilder } from '@angular/forms';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { UserLoginData } from 'app/shared/models/user.model';
import { SharedModule } from 'app/shared/shared.module';
import { selectUserLoginData } from 'app/store/login.selectors';
import { ConfirmationService, MessageService } from 'primeng/api';
import { ToastModule } from 'primeng/toast';
import { TableModule } from 'primeng/table';
import { ToolbarModule } from 'primeng/toolbar';
import { ConfirmationService, Message, MessageService } from 'primeng/api';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { FileUploadModule } from 'primeng/fileupload';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { TableModule } from 'primeng/table';
import { ToastModule } from 'primeng/toast';
import { ToolbarModule } from 'primeng/toolbar';
import { Contact, ContactTypes } from '../../shared/models/contact.model';
import { ContactDetailComponent } from '../contact-detail/contact-detail.component';
import { ContactListComponent } from './contact-list.component';
import { Contact, ContactTypes } from '../../shared/models/contact.model';

describe('ContactListComponent', () => {
let component: ContactListComponent;
Expand All @@ -24,6 +25,8 @@ describe('ContactListComponent', () => {
contact.last_name = 'Smith';
contact.name = 'ABC Inc';

let testMessageService: MessageService;

beforeEach(async () => {
const userLoginData: UserLoginData = {
committee_id: 'C00000000',
Expand All @@ -41,6 +44,7 @@ describe('ContactListComponent', () => {
DialogModule,
FileUploadModule,
ConfirmDialogModule,
SharedModule,
],
declarations: [ContactListComponent, ContactDetailComponent],
providers: [
Expand All @@ -53,6 +57,8 @@ describe('ContactListComponent', () => {
}),
],
}).compileComponents();

testMessageService = TestBed.inject(MessageService);
});

beforeEach(() => {
Expand Down Expand Up @@ -89,4 +95,19 @@ describe('ContactListComponent', () => {
name = component.displayName(contact);
expect(name).toBe('');
});

it('#onContactLookupSelect displays add msg', () => {
const testCommitteeId = "testCommitteeId";
const expectedMessage: Message = {
severity: 'success',
summary: 'Contact selected',
detail: 'Selected lookup contact ' +
'with commitee id ' + testCommitteeId,
life: 3000,
}
const messageServiceAddSpy = spyOn(testMessageService, 'add');
component.onContactLookupSelect(testCommitteeId);
expect(messageServiceAddSpy).toHaveBeenCalledOnceWith(expectedMessage);
});

});
24 changes: 21 additions & 3 deletions front-end/src/app/contacts/contact-list/contact-list.component.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component, ElementRef } from '@angular/core';
import { ConfirmationService, MessageService } from 'primeng/api';
import { TableListBaseComponent } from 'app/shared/components/table-list-base/table-list-base.component';
import { ConfirmationService, MessageService } from 'primeng/api';

import { LabelList, LabelUtils, PrimeOptions } from 'app/shared/utils/label.utils';
import { Contact, ContactTypeLabels, ContactTypes } from '../../shared/models/contact.model';
import { ContactService } from '../../shared/services/contact.service';
import { Contact, ContactTypes, ContactTypeLabels } from '../../shared/models/contact.model';
import { LabelList } from 'app/shared/utils/label.utils';

@Component({
selector: 'app-contact-list',
Expand All @@ -14,6 +14,13 @@ export class ContactListComponent extends TableListBaseComponent<Contact> {
override item: Contact = new Contact();
contactTypeLabels: LabelList = ContactTypeLabels;

searchTerm = '';

// contact lookup
contactTypeOptions: PrimeOptions = LabelUtils.getPrimeOptions(ContactTypeLabels).filter((option) =>
[ContactTypes.COMMITTEE].includes(option.code as ContactTypes)
);

constructor(
protected override messageService: MessageService,
protected override confirmationService: ConfirmationService,
Expand Down Expand Up @@ -49,4 +56,15 @@ export class ContactListComponent extends TableListBaseComponent<Contact> {
return item.name || '';
}
}

onContactLookupSelect(id: string) {
this.messageService.add({
severity: 'success',
summary: 'Contact selected',
detail: 'Selected lookup contact ' +
'with commitee id ' + id,
life: 3000,
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<form [formGroup]="contactLookupForm">
<p-dropdown
styleClass="gray"
formControlName="selectedContactType"
[options]="contactTypeOptions"
optionLabel="name"
optionValue="code">
</p-dropdown>
<p-dropdown
#lookupDropdown
class="mr-2"
(input)="onDropdownSearch($event)"
[editable]="true"
dropdownIcon="pi pi-search"
[options]="contactLookupList"
formControlName="selectedContact"
placeholder="Search for a contact"
[group]="true"
(onChange)="onContactSelect($event)">
</p-dropdown>
<span class="mr-2">or</span>
<button
pButton
pRipple
label="Create a new contact"
icon="pi pi-plus-circle"
class="p-button-info mr-2">
</button>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { EventEmitter } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { provideMockStore } from '@ngrx/store/testing';
import { CommitteeLookupResponse } from 'app/shared/models/contact.model';
import { UserLoginData } from 'app/shared/models/user.model';
import { ContactService } from 'app/shared/services/contact.service';
import { selectUserLoginData } from 'app/store/login.selectors';
import { DropdownModule } from 'primeng/dropdown';
import { of } from 'rxjs';

import { ContactLookupComponent } from './contact-lookup.component';

describe('ContactLookupComponent', () => {
let component: ContactLookupComponent;
let fixture: ComponentFixture<ContactLookupComponent>;

let testContactService: ContactService;

beforeEach(async () => {
const userLoginData: UserLoginData = {
committee_id: 'C00000000',
email: '[email protected]',
is_allowed: true,
token: 'jwttokenstring',
};

await TestBed.configureTestingModule({
declarations: [ContactLookupComponent],
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule, DropdownModule],
providers: [
FormBuilder,
ContactService,
ContactService,
EventEmitter,
provideMockStore({
initialState: { fecfile_online_userLoginData: userLoginData },
selectors: [{ selector: selectUserLoginData, value: userLoginData }],
}),
],
}).compileComponents();

testContactService = TestBed.inject(ContactService);
});

beforeEach(() => {
fixture = TestBed.createComponent(ContactLookupComponent);
component = fixture.componentInstance;

fixture.detectChanges();
});

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

it('#onDropdownSearch empty search', fakeAsync(() => {
const testEvent = { target: { value: null } };
component.onDropdownSearch(testEvent);
tick(500);
expect(component.contactLookupList.length === 0).toBeTrue();
}));

it('#onDropdownSearch happy path', fakeAsync(() => {
const testCommitteeLookupResponse = new CommitteeLookupResponse();
testCommitteeLookupResponse.fec_api_committees = [{ id: 'testId', name: 'testName' }];
testCommitteeLookupResponse.fecfile_committees = [{ id: 'testId', name: 'testName' }];
spyOn(testContactService, 'committeeLookup').and.returnValue(of(testCommitteeLookupResponse));
const testEvent = { target: { value: 'hi' } };
component.onDropdownSearch(testEvent);
tick(500);
expect(component.lookupDropdown?.overlayVisible === true).toBeTrue();
}));

it('#onContactSelect happy path', fakeAsync(() => {
const eventEmitterEmitSpy = spyOn(component.contactSelect, 'emit');
const testValue = 'testValue';
const testEvent = {
originalEvent: {
type: 'click',
},
value: testValue,
};
component.onContactSelect(testEvent);
tick(500);
expect(eventEmitterEmitSpy).toHaveBeenCalledOnceWith(testValue);
}));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
import { ContactType } from 'app/shared/models/contact.model';
import { ContactService } from 'app/shared/services/contact.service';
import { PrimeOptions } from 'app/shared/utils/label.utils';
import { debounce } from 'lodash';
import { SelectItem, SelectItemGroup } from 'primeng/api';
import { Dropdown } from 'primeng/dropdown';

@Component({
selector: 'app-contact-lookup',
templateUrl: './contact-lookup.component.html',
})
export class ContactLookupComponent {
@Input() contactTypeOptions: PrimeOptions = [];
@Input() maxFecResults = 10;
@Input() maxFecfileResults = 10;
@Input() searchDelayMillis = 250;

@Output() contactSelect = new EventEmitter<string>();

@ViewChild('lookupDropdown') lookupDropdown: Dropdown | null = null;

selectedContactType =
new FormControl<ContactType>({} as ContactType);
selectedContact: FormControl<SelectItem> | null = null;

contactLookupForm: FormGroup = this.formBuilder.group({
selectedContactType: this.selectedContactType,
selectedContact: this.selectedContact
})

contactLookupList: SelectItemGroup[] = [];

constructor(
private formBuilder: FormBuilder,
private contactService: ContactService
) { }

onDropdownSearch = debounce((event) => {
const searchTerm = event?.target?.value;
if (searchTerm) {
this.contactService.committeeLookup(
searchTerm, this.maxFecResults,
this.maxFecfileResults).subscribe((response) => {
this.contactLookupList = response &&
response.toSelectItemGroups();
if (this.lookupDropdown) {
this.lookupDropdown.overlayVisible = true;
}
});
} else {
this.contactLookupList = [];
}
}, this.searchDelayMillis);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
onContactSelect(event: any) {
if (event) {
if (event.originalEvent?.type === 'click') {
const value: string = event.value;
if (value) {
this.contactSelect.emit(value)
}
}
}
}

}
16 changes: 15 additions & 1 deletion front-end/src/app/shared/models/contact.model.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Contact, ContactTypes } from './contact.model';
import { Contact, ContactTypes, FecCommitteeLookupData } from './contact.model';

describe('Contact', () => {
it('should create an instance', () => {
Expand All @@ -18,4 +18,18 @@ describe('Contact', () => {
expect(contact.name).toBe('foo');
expect(contact.occupation).toBe(null);
});

it('#fromJSON() should return a populated FecCommitteeLookupData class', () => {
const data = {
id: "C123",
name: 'foo',
};
const fecCommitteeLookupData: FecCommitteeLookupData =
FecCommitteeLookupData.fromJSON(data);
expect(fecCommitteeLookupData).toBeInstanceOf(
FecCommitteeLookupData);
expect(fecCommitteeLookupData.id).toBe("C123");
expect(fecCommitteeLookupData.name).toBe('foo');
});

});
Loading

0 comments on commit e54be5c

Please sign in to comment.