Skip to content

Commit

Permalink
Merge pull request #669 from geonetwork/ME-persist-records-selection
Browse files Browse the repository at this point in the history
[libs, ME]: persist records selection
  • Loading branch information
Angi-Kinas authored Nov 13, 2023
2 parents 661c8b4 + d7a3c82 commit e523bd1
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ <h1 *ngIf="users" class="text-[56px] font-title grow" translate>
<gn-ui-record-table
[records]="results"
[totalHits]="users ? users.length : (searchFacade.resultsHits$ | async)"
(recordSelect)="editRecord($event)"
(recordClick)="editRecord($event)"
(recordsDeselect)="handleRecordsDeselection($event)"
(recordsSelect)="handleRecordsSelection($event)"
(sortByChange)="setSortBy($event)"
[sortBy]="searchFacade.sortBy$ | async"
[selectedRecords]="getSelectedRecords() | async"
></gn-ui-record-table>
<div
class="px-5 py-5 flex justify-center gap-8 items-baseline"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { Router } from '@angular/router'
import { BehaviorSubject } from 'rxjs'
import { CommonModule } from '@angular/common'
import { MatIconModule } from '@angular/material/icon'
import { SelectionService } from '@geonetwork-ui/api/repository/gn4'
import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures'

const results = [{ md: true }]
const currentPage = 5
Expand All @@ -22,7 +24,8 @@ const totalPages = 25
export class RecordTableComponent {
@Input() records: CatalogRecord[]
@Input() totalHits: number
@Output() recordSelect = new EventEmitter<CatalogRecord>()
@Output() recordClick = new EventEmitter<CatalogRecord>()
@Output() recordsSelect = new EventEmitter<CatalogRecord[]>()
}

@Component({
Expand Down Expand Up @@ -55,12 +58,18 @@ class RouterMock {
navigate = jest.fn()
}

class SelectionServiceMock {
selectRecords = jest.fn()
deselectRecords = jest.fn()
clearSelection = jest.fn()
}

describe('RecordsListComponent', () => {
let component: RecordsListComponent
let fixture: ComponentFixture<RecordsListComponent>
let router: Router
let searchService: SearchService
let searchFacade: SearchFacade
let selectionService: SelectionService

beforeEach(() => {
TestBed.configureTestingModule({
Expand All @@ -77,6 +86,10 @@ describe('RecordsListComponent', () => {
provide: SearchService,
useClass: SearchServiceMock,
},
{
provide: SelectionService,
useClass: SelectionServiceMock,
},
],
}).overrideComponent(RecordsListComponent, {
set: {
Expand All @@ -90,7 +103,7 @@ describe('RecordsListComponent', () => {
})
router = TestBed.inject(Router)
searchService = TestBed.inject(SearchService)
searchFacade = TestBed.inject(SearchFacade)
selectionService = TestBed.inject(SelectionService)
fixture = TestBed.createComponent(RecordsListComponent)
component = fixture.componentInstance
fixture.detectChanges()
Expand Down Expand Up @@ -119,13 +132,33 @@ describe('RecordsListComponent', () => {
expect(pagination.totalPages).toEqual(totalPages)
})
describe('when click on a record', () => {
const uniqueIdentifier = 123
const singleRecord = {
...DATASET_RECORDS[0],
uniqueIdentifier,
}
beforeEach(() => {
table.recordSelect.emit({ uniqueIdentifier: 123 })
table.recordClick.emit(singleRecord)
})
it('routes to record edition', () => {
expect(router.navigate).toHaveBeenCalledWith(['/edit', 123])
})
})
describe('when selecting a record', () => {
const uniqueIdentifier = 123
const singleRecord = {
...DATASET_RECORDS[0],
uniqueIdentifier,
}
beforeEach(() => {
table.recordsSelect.emit([singleRecord])
})
it('persists selection', () => {
expect(selectionService.selectRecords).toHaveBeenCalledWith([
singleRecord,
])
})
})
describe('when click on pagination', () => {
beforeEach(() => {
pagination.newCurrentPageEvent.emit(3)
Expand Down
17 changes: 16 additions & 1 deletion apps/metadata-editor/src/app/records/records-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { UiSearchModule } from '@geonetwork-ui/ui/search'
import { UiElementsModule } from '@geonetwork-ui/ui/elements'
import { SortByField } from '@geonetwork-ui/common/domain/search'
import { TranslateModule } from '@ngx-translate/core'
import { SelectionService } from '@geonetwork-ui/api/repository/gn4'
import { Subject } from 'rxjs'

const includes = [
'uuid',
Expand Down Expand Up @@ -45,7 +47,8 @@ export class RecordsListComponent {
constructor(
private router: Router,
public searchFacade: SearchFacade,
public searchService: SearchService
public searchService: SearchService,
private selectionService: SelectionService
) {
this.searchFacade.setPageSize(15).setConfigRequestFields(includes)
}
Expand All @@ -72,4 +75,16 @@ export class RecordsListComponent {
showUsers() {
this.router.navigate(['/users/my-org'])
}

getSelectedRecords() {
return this.selectionService.selectedRecordsIdentifiers$
}

handleRecordsSelection(records: CatalogRecord[]) {
this.selectionService.selectRecords(records).subscribe()
}

handleRecordsDeselection(records: CatalogRecord[]) {
this.selectionService.deselectRecords(records).subscribe()
}
}
1 change: 1 addition & 0 deletions libs/api/repository/src/lib/gn4/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './elasticsearch'
export * from './settings/gn4-settings.service'
export * from './auth'
export * from './favorites/favorites.service'
export * from './selection/selection.service'
105 changes: 105 additions & 0 deletions libs/api/repository/src/lib/gn4/selection/selection.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { SelectionsApiService } from '@geonetwork-ui/data-access/gn4'
import { SelectionService } from './selection.service'
import { firstValueFrom, of } from 'rxjs'
import { CatalogRecord } from '@geonetwork-ui/common/domain/record'

function record(uuid: string): CatalogRecord {
return {
uniqueIdentifier: uuid,
} as CatalogRecord
}

class SelectionsServiceMock {
private selected = ['001', '002', '003']
add = jest.fn((bucket, ids) => {
this.selected.push(...ids)
return of(undefined)
})
clear = jest.fn((bucket, ids) => {
this.selected = this.selected.filter(
(id) => !!ids && ids.indexOf(id) === -1
)
return of(undefined)
})
get = jest.fn(() => of(this.selected))
}

describe('SelectionService', () => {
let service: SelectionService
let selectionsService: SelectionsApiService

beforeEach(async () => {
selectionsService = new SelectionsServiceMock() as any
service = new SelectionService(selectionsService)
})

it('should be created', () => {
expect(service).toBeTruthy()
})

describe('#selectRecords', () => {
let selectedRecords
beforeEach(async () => {
service.selectedRecordsIdentifiers$.subscribe((value) => {
selectedRecords = value
})
await firstValueFrom(
service.selectRecords([record('abcd'), record('efgh'), record('001')])
)
})
it('calls the corresponding API', () => {
expect(selectionsService.add).toHaveBeenCalledWith('gnui', [
'abcd',
'efgh',
'001',
])
})
it('emits new records in selectedRecordsIdentifiers$', () => {
expect(selectedRecords).toEqual(['001', '002', '003', 'abcd', 'efgh'])
})
})

describe('#deselectRecords', () => {
let selectedRecords
beforeEach(async () => {
service.selectedRecordsIdentifiers$.subscribe((value) => {
selectedRecords = value
})
await firstValueFrom(
service.deselectRecords([record('abcd'), record('efgh'), record('001')])
)
})
it('calls the corresponding API', () => {
expect(selectionsService.clear).toHaveBeenCalledWith('gnui', [
'abcd',
'efgh',
'001',
])
})
it('emits new records in selectedRecordsIdentifiers$', () => {
expect(selectedRecords).toEqual(['002', '003'])
})
})

describe('#clearSelection', () => {
let selectedRecords
beforeEach(async () => {
service.selectedRecordsIdentifiers$.subscribe((value) => {
selectedRecords = value
})
await firstValueFrom(service.clearSelection())
})
it('calls the corresponding API', () => {
expect(selectionsService.get).toHaveBeenCalledWith('gnui')

expect(selectionsService.clear).toHaveBeenCalledWith('gnui', [
'001',
'002',
'003',
])
})
it('emits new records in selectedRecordsIdentifiers$', () => {
expect(selectedRecords).toEqual([])
})
})
})
80 changes: 80 additions & 0 deletions libs/api/repository/src/lib/gn4/selection/selection.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Injectable } from '@angular/core'
import { CatalogRecord } from '@geonetwork-ui/common/domain/record'
import { SelectionsApiService } from '@geonetwork-ui/data-access/gn4'
import { BehaviorSubject, Observable, Subscription, map, tap } from 'rxjs'

const BUCKET_ID = 'gnui'

@Injectable({
providedIn: 'root',
})
export class SelectionService {
selectedRecordsIdentifiers$: BehaviorSubject<string[]> = new BehaviorSubject(
[]
)
subscription: Subscription

constructor(private selectionsApi: SelectionsApiService) {
this.selectionsApi.get(BUCKET_ID).subscribe((selectedIds) => {
this.addIdsToSelected(Array.from(selectedIds))
})
}

private addIdsToSelected(ids: string[]) {
const currentIds = this.selectedRecordsIdentifiers$.value
const uniqueSet = new Set([...currentIds, ...ids])
this.selectedRecordsIdentifiers$.next([...uniqueSet])
}

private removeIdsFromSelected(ids: string[]) {
const filtered = this.selectedRecordsIdentifiers$.value.filter(
(value) => !ids.includes(value)
)
this.selectedRecordsIdentifiers$.next(filtered)
}

selectRecords(records: CatalogRecord[]): Observable<void> {
const newIds = []
records.map((record) => {
newIds.push(record.uniqueIdentifier)
})
const apiResponse = this.selectionsApi.add(BUCKET_ID, newIds)
return apiResponse.pipe(
tap(() => {
this.addIdsToSelected(newIds)
}),
map(() => undefined)
)
}

deselectRecords(records: CatalogRecord[]): Observable<void> {
const idsToBeRemoved = []
records.map((record) => {
idsToBeRemoved.push(record.uniqueIdentifier)
})
const apiResponse = this.selectionsApi.clear(BUCKET_ID, idsToBeRemoved)
return apiResponse.pipe(
tap(() => {
this.removeIdsFromSelected(idsToBeRemoved)
}),
map(() => undefined)
)
}

clearSelection(): Observable<void> {
const currentSelectedResponse = this.selectionsApi.get(BUCKET_ID)
let currentSelection
this.subscription = currentSelectedResponse.subscribe((value) => {
currentSelection = [...value]
})
this.selectionsApi.clear(BUCKET_ID, currentSelection)
const apiResponse = this.selectionsApi.clear(BUCKET_ID, currentSelection)

return apiResponse.pipe(
tap(() => {
this.removeIdsFromSelected(currentSelection)
}),
map(() => undefined)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('RecordTableComponent', () => {
})
})

describe('isChecked', () => {
describe('#isChecked', () => {
it('should return true when the record is in the selectedRecords array', () => {
const component = new RecordTableComponent()
component.selectedRecords = ['1', '2', '3']
Expand Down
17 changes: 11 additions & 6 deletions libs/ui/search/src/lib/record-table/record-table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ import { SortByField } from '@geonetwork-ui/common/domain/search'
styleUrls: ['./record-table.component.css'],
})
export class RecordTableComponent {
selectedRecords: string[] = []

@Input() selectedRecords: string[] = []
@Input() records: any[] = []
@Input() totalHits?: number
@Input() sortBy?: SortByField
@Output() recordSelect = new EventEmitter<CatalogRecord>()
@Output() recordClick = new EventEmitter<CatalogRecord>()
@Output() recordsSelect = new EventEmitter<CatalogRecord[]>()
@Output() recordsDeselect = new EventEmitter<CatalogRecord[]>()
@Output() sortByChange = new EventEmitter<SortByField>()

dateToString(date: Date): string {
Expand Down Expand Up @@ -102,21 +103,25 @@ export class RecordTableComponent {

handleRecordSelectedChange(selected: boolean, record: CatalogRecord) {
if (!selected) {
this.recordsDeselect.emit([record])
this.selectedRecords = this.selectedRecords.filter(
(val) => val !== record.uniqueIdentifier
)
} else {
this.recordsSelect.emit([record])
this.selectedRecords.push(record.uniqueIdentifier)
}
}

selectAll() {
if (this.isAllSelected()) {
this.recordsDeselect.emit(this.records)
this.selectedRecords = []
} else {
this.selectedRecords = this.records.map(
(record) => record.uniqueIdentifier
)
this.recordsSelect.emit(this.records)
this.selectedRecords = this.records.map((record) => {
return record.uniqueIdentifier
})
}
}

Expand Down

0 comments on commit e523bd1

Please sign in to comment.