Skip to content

Commit

Permalink
Merge pull request #717 from geonetwork/orgs-filter
Browse files Browse the repository at this point in the history
[Datahub] Add search filter for organisations
  • Loading branch information
fgravin authored Dec 18, 2023
2 parents beedacc + 001d99d commit 19be416
Show file tree
Hide file tree
Showing 26 changed files with 372 additions and 47 deletions.
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

0 comments on commit 19be416

Please sign in to comment.