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

[Datahub] Add search filter for organisations #717

Merged
merged 15 commits into from
Dec 18, 2023
Merged
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
43 changes: 41 additions & 2 deletions apps/datahub-e2e/src/e2e/organizations.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('organizations', () => {
cy.visit('/home/organisations')

// aliases
cy.get('gn-ui-organisations-sort')
cy.get('gn-ui-organisations-filter')
.find('gn-ui-dropdown-selector')
.as('sort')
cy.get('gn-ui-pagination').children('div').as('pagination')
Expand All @@ -21,6 +21,12 @@ describe('organizations', () => {
cy.get('@organizations')
.find('[data-cy="organizationRecordsCount"]')
.as('organizationsRecordsCount')
cy.get('gn-ui-organisations-filter')
.find('gn-ui-search-input')
.as('organisationsSearch')
cy.get('gn-ui-organisations')
.find('gn-ui-organisations-result')
.as('organisationsResult')
})

describe('general display', () => {
Expand All @@ -32,7 +38,7 @@ describe('organizations', () => {
.should('eq', 'decoration-primary')
})
it('should display the welcome panel', () => {
cy.get('gn-ui-organisations-sort').should('be.visible')
cy.get('gn-ui-organisations-filter').should('be.visible')
cy.get('@sort').openDropdown().children('button').should('have.length', 4)
})
it('should display organizations with thumbnail, title and description', () => {
Expand Down Expand Up @@ -135,4 +141,37 @@ describe('organizations', () => {
})
})
})

describe('search filter', () => {
it('should display filtered results ignoring accents and case', () => {
cy.get('@organisationsSearch').type('geo2france')
cy.get('@organizationsName').should('have.length', 1)
cy.get('@organisationsResult').should('contain', '1')
cy.get('@organizationsName')
.eq(0)
.invoke('text')
.should('contain', 'Géo2France')
})
it('should display filtered results containing multiple words', () => {
cy.get('@organisationsSearch').type('dreal hdf')
cy.get('@organizationsName').should('have.length', 1)
cy.get('@organisationsResult').should('contain', '1')
cy.get('@organizationsName')
.eq(0)
.invoke('text')
.should('contain', 'DREAL HdF')
})
it('should display multiple results and refine search', () => {
cy.get('@organisationsSearch').type('de')
cy.get('@organizationsName').should('have.length', 10)
cy.get('@organisationsResult').should('contain', '10')
cy.get('@organisationsSearch').type(' Lille')
cy.get('@organizationsName').should('have.length', 1)
cy.get('@organisationsResult').should('contain', '1')
})
it('should display a message for no results found', () => {
cy.get('@organisationsSearch').type('An organisation that does not exist')
cy.get('@organisationsResult').should('contain', 'No organizations found')
})
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<gn-ui-organisations-sort
<gn-ui-organisations-filter
(sortBy)="setSortBy($event)"
></gn-ui-organisations-sort>
(filterBy)="setFilterBy($event)"
></gn-ui-organisations-filter>
<div class="mt-6 rounded-lg text-gray-800 p-4 bg-slate-100">
<gn-ui-organisations-result
*ngIf="organisationsTotal$ | async"
[hits]="organisationResults"
[total]="organisationsTotal$ | async"
></gn-ui-organisations-result>
</div>
<div
class="grid grid-cols-1 mt-6 gap-x-6 gap-y-8 sm:grid-cols-2 lg:grid-cols-3"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import { OrganisationsComponent } from './organisations.component'
import { OrganizationsServiceInterface } from '@geonetwork-ui/common/domain/organizations.service.interface'

@Component({
selector: 'gn-ui-organisations-sort',
selector: 'gn-ui-organisations-filter',
template: '<div></div>',
})
class OrganisationsSortMockComponent {
class OrganisationsFilterMockComponent {
@Output() sortBy = new EventEmitter<string>()
}
@Component({
Expand All @@ -31,6 +31,15 @@ class OrganisationPreviewMockComponent {
@Output() clickedOrganisation = new EventEmitter<Organization>()
}

@Component({
selector: 'gn-ui-organisations-result',
template: '<div></div>',
})
class OrganisationsResultMockComponent {
@Input() hits: number
@Input() total: number
}

@Component({
selector: 'gn-ui-pagination',
template: '<div></div>',
Expand All @@ -43,6 +52,7 @@ class PaginationMockComponent {

class OrganisationsServiceMock {
organisations$ = of(ORGANISATIONS_FIXTURE)
organisationsCount$ = of(ORGANISATIONS_FIXTURE.length)
}

const organisationMock = {
Expand All @@ -63,9 +73,10 @@ describe('OrganisationsComponent', () => {
await TestBed.configureTestingModule({
declarations: [
OrganisationsComponent,
OrganisationsSortMockComponent,
OrganisationsFilterMockComponent,
OrganisationPreviewMockComponent,
PaginationMockComponent,
OrganisationsResultMockComponent,
ContentGhostComponent,
],
providers: [
Expand Down Expand Up @@ -93,6 +104,7 @@ describe('OrganisationsComponent', () => {

describe('on component init', () => {
let orgPreviewComponents: OrganisationPreviewMockComponent[]
let orgResultComponent: OrganisationsResultMockComponent
let paginationComponentDE: DebugElement
let setCurrentPageSpy
let setSortBySpy
Expand Down Expand Up @@ -156,7 +168,7 @@ describe('OrganisationsComponent', () => {
beforeEach(() => {
setSortBySpy = jest.spyOn(component, 'setSortBy')
de.query(
By.directive(OrganisationsSortMockComponent)
By.directive(OrganisationsFilterMockComponent)
).triggerEventHandler('sortBy', ['desc', 'recordCount'])
fixture.detectChanges()
orgPreviewComponents = de
Expand All @@ -181,6 +193,69 @@ describe('OrganisationsComponent', () => {
)
})
})
describe('filter organisations', () => {
describe('initial state', () => {
beforeEach(() => {
orgResultComponent = de.query(
By.directive(OrganisationsResultMockComponent)
)
})
it('should display number of organisations found to equal all', () => {
expect(orgResultComponent.componentInstance.hits).toEqual(
ORGANISATIONS_FIXTURE.length
)
})
it('should display number of all organisations', () => {
expect(orgResultComponent.componentInstance.total).toEqual(
ORGANISATIONS_FIXTURE.length
)
})
})
describe('entering search terms', () => {
beforeEach(() => {
orgResultComponent = de.query(
By.directive(OrganisationsResultMockComponent)
)
})
it('should ignore case and display 11 matches for "Data", "DATA" or "data"', () => {
component.filterBy$.next('Data')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(11)
component.filterBy$.next('DATA')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(11)
component.filterBy$.next('data')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(11)
})
it('should ignore accents and case and display 2 matches for "é Data Org", "e Data Org" or "E Data Org"', () => {
component.filterBy$.next('é Data Org')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(2)
component.filterBy$.next('e Data Org')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(2)
component.filterBy$.next('E Data Org')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(2)
})
it('should combine multiple termes with "AND" logic and display 1 match for "a data"', () => {
component.filterBy$.next('a data')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(1)
})
it('should combine multiple termes with "AND" logic and display 11 matches for "data org"', () => {
component.filterBy$.next('data org')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(11)
})
it('should display 12 matches for "ORG"', () => {
component.filterBy$.next('ORG')
fixture.detectChanges()
expect(orgResultComponent.componentInstance.hits).toEqual(12)
})
})
})
describe('click on organisation', () => {
let orgSelected
beforeEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,34 @@ export class OrganisationsComponent {

totalPages: number
currentPage$ = new BehaviorSubject(1)
organisationResults: number
sortBy$: BehaviorSubject<SortByField> = new BehaviorSubject(['asc', 'name'])

organisationsSorted$: Observable<Organization[]> = combineLatest([
filterBy$: BehaviorSubject<string> = new BehaviorSubject('')
organisationsTotal$ = this.organisationsService.organisationsCount$
organisationsFilteredAndSorted$: Observable<Organization[]> = combineLatest([
this.organisationsService.organisations$.pipe(
startWith(Array(this.itemsOnPage).fill({}))
),
this.sortBy$,
this.filterBy$,
]).pipe(
map(([organisations, sortBy]) =>
this.sortOrganisations(organisations, sortBy)
)
map(([organisations, sortBy, filterBy]) => {
const filteredOrganisations = this.filterOrganisations(
organisations,
filterBy
)
return this.sortOrganisations(filteredOrganisations, sortBy)
})
)

organisations$: Observable<Organization[]> = combineLatest([
this.organisationsSorted$,
this.organisationsFilteredAndSorted$,
this.currentPage$,
]).pipe(
tap(
([organisations]) =>
(this.totalPages = Math.ceil(organisations.length / this.itemsOnPage))
),
tap(([organisations]) => {
this.organisationResults = organisations.length
this.totalPages = Math.ceil(organisations.length / this.itemsOnPage)
}),
map(([organisations, page]) =>
organisations.slice(
(page - 1) * this.itemsOnPage,
Expand All @@ -66,10 +73,35 @@ export class OrganisationsComponent {
this.currentPage$.next(page)
}

protected setFilterBy(value: string): void {
this.filterBy$.next(value)
}

protected setSortBy(value: SortByField): void {
this.sortBy$.next(value)
}

private filterOrganisations(organisations: Organization[], filterBy: string) {
if (!filterBy) return organisations
const filterRegex = new RegExp(
this.normalizeString(filterBy) //ignore accents and case
.replace(/[^a-z0-9\s]/g, '') //ignore special characters
.replace(/\s(?=.)/g, '.*') //replace whitespaces by "AND" separator
.replace(/\s/g, ''), //remove potential whitespaces left
'i'
)
return [...organisations].filter((org) => {
return this.normalizeString(org.name).match(filterRegex)
})
}

private normalizeString(str: string) {
return str
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
}

private sortOrganisations(
organisations: Organization[],
sortBy: SortByField
Expand Down
2 changes: 1 addition & 1 deletion libs/ui/catalog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export * from './lib/ui-catalog.module'
export * from './lib/catalog-title/catalog-title.component'
export * from './lib/language-switcher/language-switcher.component'
export * from './lib/organisation-preview/organisation-preview.component'
export * from './lib/organisations-sort/organisations-sort.component'
export * from './lib/organisations-filter/organisations-filter.component'
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<div
class="flex flex-wrap sm:flex-nowrap bg-primary text-white px-6 py-7 mt-4 mb-8 rounded-lg"
class="flex flex-wrap sm:flex-nowrap justify-between bg-white shadow-xl p-5 rounded-lg"
>
<span class="grow mb-4 mr-4 sm:mb-0 sm:mr-16">
<p translate>organisation.sort.intro</p>
<span class="grow mb-4 mr-4 sm:mb-0 sm:mr-16 sm:max-w-sm">
<gn-ui-search-input
(valueChange)="filterOrganisations($event)"
[placeholder]="'organisation.filter.placeholder' | translate"
></gn-ui-search-input>
</span>
<span class="flex flex-wrap sm:flex-nowrap sm:shrink-0">
<gn-ui-dropdown-selector
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { OrganisationsSortComponent } from './organisations-sort.component'
import { OrganisationsFilterComponent } from './organisations-filter.component'
import { Component, EventEmitter, Input, Output } from '@angular/core'
import { TranslateModule } from '@ngx-translate/core'

Expand All @@ -18,16 +18,19 @@ class DropdownSelectorMockComponent {
}

describe('OrganisationsOrderComponent', () => {
let component: OrganisationsSortComponent
let fixture: ComponentFixture<OrganisationsSortComponent>
let component: OrganisationsFilterComponent
let fixture: ComponentFixture<OrganisationsFilterComponent>

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OrganisationsSortComponent, DropdownSelectorMockComponent],
declarations: [
OrganisationsFilterComponent,
DropdownSelectorMockComponent,
],
imports: [TranslateModule.forRoot()],
}).compileComponents()

fixture = TestBed.createComponent(OrganisationsSortComponent)
fixture = TestBed.createComponent(OrganisationsFilterComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import {
TRANSLATE_DEFAULT_CONFIG,
UtilI18nModule,
} from '@geonetwork-ui/util/i18n'
import { OrganisationsSortComponent } from './organisations-sort.component'
import { OrganisationsFilterComponent } from './organisations-filter.component'
import { DropdownSelectorComponent } from '@geonetwork-ui/ui/inputs'

export default {
title: 'Catalog/OrganisationsSortComponent',
component: OrganisationsSortComponent,
title: 'Catalog/OrganisationsFilterComponent',
component: OrganisationsFilterComponent,
decorators: [
moduleMetadata({
declarations: [DropdownSelectorComponent],
Expand All @@ -27,6 +27,6 @@ export default {
(story) => `<div style="max-width: 1000px">${story}</div>`
),
],
} as Meta<OrganisationsSortComponent>
} as Meta<OrganisationsFilterComponent>

export const Primary: StoryObj<OrganisationsSortComponent> = {}
export const Primary: StoryObj<OrganisationsFilterComponent> = {}
Loading
Loading