Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cb2-8144): ability to search reference data #1086

Merged
merged 21 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1cfecbd
feat(cb2-8144): working search functionality
owen-corrigan-bjss Jul 20, 2023
e468c75
feat(cb2-8168): searches every field
owen-corrigan-bjss Jul 21, 2023
5600849
feat(cb2-8168): clears search and returns to page 1
owen-corrigan-bjss Jul 25, 2023
f929664
feat(cb2-8168): resets form after search
owen-corrigan-bjss Jul 25, 2023
fe5e43d
feat(cb2-8168): adds tests for reference data selector
owen-corrigan-bjss Jul 25, 2023
a68e413
feat(cb2-8144): adds navigate to deleted item test
owen-corrigan-bjss Jul 25, 2023
6ff1951
feat(cb2-8144): merge develop
owen-corrigan-bjss Jul 25, 2023
062f43d
feat(cb2-8144): displays error if there are no search results
owen-corrigan-bjss Jul 25, 2023
6780d17
feat(cb2-8144): trying to fix linting issue
owen-corrigan-bjss Jul 25, 2023
94ede89
feat(cb2-8144): attempt to fix lint issue
owen-corrigan-bjss Jul 25, 2023
c64da06
feat(cb2-8144): removes some subscriptions
owen-corrigan-bjss Jul 26, 2023
9a68df3
feat(cb2-8144): removes unused async
owen-corrigan-bjss Jul 26, 2023
7050810
feat(cb2-8144): organised imports
owen-corrigan-bjss Jul 26, 2023
346916f
feat(cb2-8144): changed selector back
owen-corrigan-bjss Jul 26, 2023
3824291
feat(cb2-8144): fixes failing test
owen-corrigan-bjss Jul 26, 2023
ff43e35
feat(cb2-8144): removed a unnessesary disabled on clear button
owen-corrigan-bjss Jul 26, 2023
85acb9a
feat(cb2-8144): a little closer to desired styling
owen-corrigan-bjss Jul 26, 2023
df83e2c
feat(cb2-8144): better layout achieved
owen-corrigan-bjss Jul 26, 2023
2bb8c64
feat(cb2-8144): added some tests for new way of navigating
owen-corrigan-bjss Jul 26, 2023
af2041d
feat(cb2-8144): tweek to css
owen-corrigan-bjss Jul 26, 2023
e24bcea
feat(cb2-8144): removes some unused imports
owen-corrigan-bjss Jul 27, 2023
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
<div class="govuk-grid-row" *appRoleRequired="roles.ReferenceDataView">
<div class="govuk-grid-column-full">
<p class="govuk-heading-xl govuk-!-font-weight-bold">Search for {{ (refDataAdminType$ | async).label }}</p>
<p class="govuk-heading-xl govuk-!-font-weight-bold" id="heading">Search for {{ (refDataAdminType$ | async).label }}</p>

<app-button-group>
<app-button id="submit" (clicked)="addNew()">Add New</app-button>
<app-button id="submit" [disabled]="disabled" (clicked)="navigateToDeletedItems()">Deleted Items</app-button>
</app-button-group>
<div id="buttonsAndSearch">
<app-button-group id="addCreateButtons">
<app-button id="submit" (clicked)="addNew()">Add New</app-button>
<app-button id="submit" [disabled]="disabled" (clicked)="navigateToDeletedItems()">Deleted Items</app-button>
</app-button-group>

<div [formGroup]="form" class="govuk-form-group" id="searchForm">
<app-select
#searchFilter
label="Search filter"
[formControlName]="form.meta.children![0].name"
[name]="form.meta.children![0].name"
[options]="(refDataAdminType$ | async).searchOptions"
></app-select>

<br />
<input
id="refSearch"
#searchTerm
[formControlName]="form.meta.children![1].name"
[name]="form.meta.children![1].name"
aria-describedby="term"
type="text"
class="govuk-input govuk-!-width-two-thirds"
placeholder="Search Term"
/>
<app-button id="searchSubmit" type="button" (clicked)="search(searchTerm.value, searchFilter.value)">Search</app-button>
<app-button id="searchCancel" type="button" design="warning" (clicked)="clear()" [disabled]="searchTerm.value ? false : true"
>Clear</app-button
>
</div>
</div>

<table class="govuk-table">
<thead class="govuk-table__head">
Expand All @@ -32,6 +60,7 @@
[numberOfItems]="(numberOfRecords$ | async) ?? 0"
(paginationOptions)="handlePaginationChange($event)"
[itemsPerPage]="25"
[reset]="currentPage"
></app-pagination>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
#buttons {
width: 18%;
}

#buttonsAndSearch {
display: flex;
width: 100%;
}

#addCreateButtons {
width: 50%;
}

#refSearch {
width: 220px !important;
margin-right: 15px;
}

#searchForm {
width: 50%;
padding: 5px;
margin-bottom: 0px;
padding-bottom: 15px;
}

#searchSubmit {
margin-right: 15px;
}

#addCreateButtons {
display: flex;
align-items: flex-end;
padding-top: 10px !important;
padding-bottom: 0px;
}

#heading {
margin-bottom: 10px !important;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,31 @@ import { ReferenceDataListComponent } from './reference-data-list.component';
import { ReferenceDataService } from '@services/reference-data/reference-data.service';
import { UserService } from '@services/user-service/user-service';
import { ReferenceDataResourceType } from '@models/reference-data.model';
import { GlobalErrorService } from '@core/components/global-error/global-error.service';
import { addError } from '@store/global-error/actions/global-error.actions';
import { GlobalErrorComponent } from '@core/components/global-error/global-error.component';
import * as refSelectors from '../../../store/reference-data/selectors/reference-data.selectors';
import { of } from 'rxjs';
import { createSelector } from '@ngrx/store';

describe('DataTypeListComponent', () => {
let component: ReferenceDataListComponent;
let fixture: ComponentFixture<ReferenceDataListComponent>;
let store: MockStore<State>;
let router: Router;
let errorService: GlobalErrorService;
let route: ActivatedRoute;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ReferenceDataListComponent],
imports: [RouterTestingModule, HttpClientTestingModule],
providers: [provideMockStore({ initialState: initialAppState }), ReferenceDataService, { provide: UserService, useValue: {} }]
providers: [
provideMockStore({ initialState: initialAppState }),
ReferenceDataService,
GlobalErrorService,
{ provide: UserService, useValue: {} }
]
}).compileComponents();
});

Expand All @@ -29,6 +41,8 @@ describe('DataTypeListComponent', () => {
component = fixture.componentInstance;
route = TestBed.inject(ActivatedRoute);
router = TestBed.inject(Router);
store = TestBed.inject(MockStore);
errorService = TestBed.inject(GlobalErrorService);
fixture.detectChanges();
});

Expand Down Expand Up @@ -62,4 +76,79 @@ describe('DataTypeListComponent', () => {
expect(navigateSpy).toBeCalledWith(['create'], { relativeTo: route });
});
});
describe('navigateToDeletedItems', () => {
it('should navigate to the selected items create', () => {
const navigateSpy = jest.spyOn(router, 'navigate');

component.navigateToDeletedItems();

expect(navigateSpy).toBeCalledWith(['deleted-items'], { relativeTo: route });
});
});
describe('handlePaginationChange', () => {
it('should set the start and end pages', () => {
component.handlePaginationChange({ start: 0, end: 24 });

expect(component.pageStart).toBe(0);
expect(component.pageEnd).toBe(24);
});
});
describe('clear', () => {
it('should reset the form', () => {
component.form.controls['term'].patchValue('foo');
expect(component.form.controls['term'].value).toBe('foo');

component.clear();
expect(component.form.controls['term'].value).toBe(null);
});
it('should reset the start and end page if search returned is true', () => {
component.handlePaginationChange({ start: 13, end: 17 });
expect(component.pageStart).toBe(13);
expect(component.pageEnd).toBe(17);

component.searchReturned = true;
component.clear();

expect(component.pageStart).toBe(0);
expect(component.pageEnd).toBe(24);
});
it('should not reset start and end page if search returned is false', () => {
component.handlePaginationChange({ start: 13, end: 17 });
expect(component.pageStart).toBe(13);
expect(component.pageEnd).toBe(17);

component.searchReturned = false;
component.clear();

expect(component.pageStart).toBe(13);
expect(component.pageEnd).toBe(17);
});
});

describe('search', () => {
it('it should call add error if there is no search term', () => {
const errorSpy = jest.spyOn(errorService, 'addError');
component.search('', 'tyreCode');

expect(errorSpy).toBeCalled();
});
it('it should call add error if there is no filter', () => {
const errorSpy = jest.spyOn(errorService, 'addError');
component.search('term', '');

expect(errorSpy).toBeCalled();
});
it('should call set data', () => {
const storeSpy = jest.spyOn(refSelectors, 'selectRefDataBySearchTerm').mockReturnValue(
createSelector(
v => v,
() => [{ resourceKey: 'foo', resourceType: 'bar' }]
)
);

component.search('term ', 'tyreCode');

expect(component.data).toEqual([{ resourceKey: 'foo', resourceType: 'bar' }]);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,34 +1,63 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { GlobalErrorService } from '@core/components/global-error/global-error.service';
import { DynamicFormService } from '@forms/services/dynamic-form.service';
import { CustomFormGroup, FormNode, FormNodeTypes } from '@forms/services/dynamic-form.types';
import { ReferenceDataModelBase, ReferenceDataResourceType } from '@models/reference-data.model';
import { Roles } from '@models/roles.enum';
import { select, Store } from '@ngrx/store';
import { Store, select } from '@ngrx/store';
import { ReferenceDataService } from '@services/reference-data/reference-data.service';
import { selectAllReferenceDataByResourceType, selectReferenceDataByResourceKey } from '@store/reference-data';
import { catchError, filter, map, Observable, of, switchMap, take } from 'rxjs';
import { PaginationComponent } from '@shared/components/pagination/pagination.component';
import { selectAllReferenceDataByResourceType, selectRefDataBySearchTerm, selectReferenceDataByResourceKey } from '@store/reference-data';
import { Observable, Subject, catchError, filter, map, of, switchMap, take, takeUntil } from 'rxjs';

@Component({
selector: 'app-reference-data-list',
templateUrl: './reference-data-list.component.html',
styleUrls: ['./reference-data-list.component.scss']
})
export class ReferenceDataListComponent implements OnInit {
export class ReferenceDataListComponent implements OnInit, OnDestroy {
type!: ReferenceDataResourceType;
disabled: boolean = true;
pageStart?: number;
pageEnd?: number;
currentPage?: number;
data: Array<ReferenceDataModelBase> | undefined;
private destroy$ = new Subject<void>();

public form!: CustomFormGroup;
public searchReturned: boolean = false;

public searchTemplate: FormNode = {
name: 'criteria',
type: FormNodeTypes.GROUP,
children: [
{
name: 'filter',
label: 'Search filter',
value: '',
type: FormNodeTypes.CONTROL
},
{
name: 'term',
value: '',
type: FormNodeTypes.CONTROL
}
]
};

constructor(
private referenceDataService: ReferenceDataService,
private route: ActivatedRoute,
private router: Router,
private store: Store,
private cdr: ChangeDetectorRef,
private globalErrorService: GlobalErrorService
private globalErrorService: GlobalErrorService,
public dfs: DynamicFormService
) {}

ngOnInit(): void {
this.form = this.dfs.createForm(this.searchTemplate) as CustomFormGroup;
this.route.params.pipe(take(1)).subscribe(params => {
this.type = params['type'];
this.referenceDataService.loadReferenceData(this.type);
Expand Down Expand Up @@ -61,16 +90,13 @@ export class ReferenceDataListComponent implements OnInit {
.subscribe({
next: res => of(!!res)
});
this.store.pipe(select(selectAllReferenceDataByResourceType(this.type)), takeUntil(this.destroy$)).subscribe(items => (this.data = items));
owen-corrigan-bjss marked this conversation as resolved.
Show resolved Hide resolved
}

get refDataAdminType$(): Observable<any | undefined> {
return this.store.pipe(select(selectReferenceDataByResourceKey(ReferenceDataResourceType.ReferenceDataAdminType, this.type)));
}

get data$(): Observable<any[] | undefined> {
return this.store.pipe(select(selectAllReferenceDataByResourceType(this.type)));
}

public get roles(): typeof Roles {
return Roles;
}
Expand Down Expand Up @@ -104,10 +130,48 @@ export class ReferenceDataListComponent implements OnInit {
}

get paginatedItems$(): Observable<any[]> {
return this.data$.pipe(map(items => items?.slice(this.pageStart, this.pageEnd) ?? []));
return of(this.data?.slice(this.pageStart, this.pageEnd) ?? []);
}

get numberOfRecords$(): Observable<number> {
return this.data$.pipe(map(items => items?.length ?? 0));
return of(this.data?.length ?? 0);
}

search(term: string, filter: string) {
this.globalErrorService.clearErrors();
const trimmedTerm = term?.trim();
if (!trimmedTerm || !filter) {
const error = !trimmedTerm ? 'You must provide a search criteria' : 'You must select a valid search filter';
this.globalErrorService.addError({ error, anchorLink: 'term' });
return;
}

this.currentPage = 1;
this.store.pipe(select(selectRefDataBySearchTerm(trimmedTerm, this.type, filter)), take(1)).subscribe(items => {
if (items.length === 0) {
this.globalErrorService.addError({ error: 'Your search returned no results', anchorLink: 'term' });
} else {
this.searchReturned = true;
this.data = items;
this.handlePaginationChange({ start: 0, end: 24 });
this.currentPage = undefined;
}
});
}

clear() {
this.form.reset();
this.globalErrorService.clearErrors();
if (this.searchReturned) {
this.currentPage = 1;
this.store.pipe(select(selectAllReferenceDataByResourceType(this.type)), take(1)).subscribe(items => (this.data = items));
this.handlePaginationChange({ start: 0, end: 24 });
this.currentPage = undefined;
}
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
6 changes: 6 additions & 0 deletions src/app/shared/components/pagination/pagination.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ export class PaginationComponent implements OnInit, OnDestroy {
@Input() tableName!: string;
@Input() numberOfItems: number = 0;
@Input() itemsPerPage: number = 5;
@Input() set reset(page: number | undefined) {
if (page) {
this.currentPage = page;
}
}
@Output() paginationOptions = new EventEmitter<{ currentPage: number; itemsPerPage: number; start: number; end: number }>();

currentPage = 1;
currentPageSubject = new ReplaySubject<number>(this.currentPage);
numberOfVisiblePages = 5;
_pages?: Array<number>;

private destroy$ = new Subject<void>();

Expand Down
Loading