Skip to content

Commit

Permalink
Merge pull request #942 from geonetwork/ME/duplicate-local-record
Browse files Browse the repository at this point in the history
ME: duplicate local record
  • Loading branch information
LHBruneton-C2C authored Jul 24, 2024
2 parents 00c93be + 6a2ba38 commit 613f731
Show file tree
Hide file tree
Showing 31 changed files with 418 additions and 23 deletions.
6 changes: 6 additions & 0 deletions apps/metadata-editor/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SearchRecordsComponent } from './records/search-records/search-records-
import { MyOrgUsersComponent } from './my-org-users/my-org-users.component'
import { MyOrgRecordsComponent } from './records/my-org-records/my-org-records.component'
import { NewRecordResolver } from './new-record.resolver'
import { DuplicateRecordResolver } from './duplicate-record.resolver'

export const appRoutes: Route[] = [
{ path: '', redirectTo: 'catalog/search', pathMatch: 'prefix' },
Expand Down Expand Up @@ -101,6 +102,11 @@ export const appRoutes: Route[] = [
component: EditPageComponent,
resolve: { record: NewRecordResolver },
},
{
path: 'duplicate/:uuid',
component: EditPageComponent,
resolve: { record: DuplicateRecordResolver },
},
{
path: 'edit/:uuid',
component: EditPageComponent,
Expand Down
84 changes: 84 additions & 0 deletions apps/metadata-editor/src/app/duplicate-record.resolver.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { TestBed } from '@angular/core/testing'
import { DuplicateRecordResolver } from './duplicate-record.resolver'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NotificationsService } from '@geonetwork-ui/feature/notifications'
import { of, throwError } from 'rxjs'
import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures'
import { ActivatedRouteSnapshot, convertToParamMap } from '@angular/router'
import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
import { TranslateModule } from '@ngx-translate/core'
import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface'

class NotificationsServiceMock {
showNotification = jest.fn()
}
class RecordsRepositoryMock {
openRecordForDuplication = jest.fn(() =>
of([DATASET_RECORDS[0], '<xml>blabla</xml>', false])
)
}

const activatedRoute = {
paramMap: convertToParamMap({ id: DATASET_RECORDS[0].uniqueIdentifier }),
} as ActivatedRouteSnapshot

describe('DuplicateRecordResolver', () => {
let resolver: DuplicateRecordResolver
let recordsRepository: RecordsRepositoryInterface
let notificationsService: NotificationsService
let resolvedData: [CatalogRecord, string, boolean]

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, TranslateModule.forRoot()],
providers: [
{ provide: NotificationsService, useClass: NotificationsServiceMock },
{
provide: RecordsRepositoryInterface,
useClass: RecordsRepositoryMock,
},
],
})
resolver = TestBed.inject(DuplicateRecordResolver)
recordsRepository = TestBed.inject(RecordsRepositoryInterface)
notificationsService = TestBed.inject(NotificationsService)
})

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

describe('load record success', () => {
beforeEach(() => {
resolvedData = undefined
resolver.resolve(activatedRoute).subscribe((r) => (resolvedData = r))
})
it('should load record by uuid', () => {
expect(resolvedData).toEqual([
DATASET_RECORDS[0],
'<xml>blabla</xml>',
false,
])
})
})

describe('load record failure', () => {
beforeEach(() => {
recordsRepository.openRecordForDuplication = () =>
throwError(() => new Error('oopsie'))
resolvedData = undefined
resolver.resolve(activatedRoute).subscribe((r) => (resolvedData = r))
})
it('should not emit anything', () => {
expect(resolvedData).toBeUndefined()
})
it('should show error notification', () => {
expect(notificationsService.showNotification).toHaveBeenCalledWith({
type: 'error',
title: 'editor.record.loadError.title',
text: 'editor.record.loadError.body oopsie',
closeMessage: 'editor.record.loadError.closeMessage',
})
})
})
})
42 changes: 42 additions & 0 deletions apps/metadata-editor/src/app/duplicate-record.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Injectable } from '@angular/core'
import { ActivatedRouteSnapshot } from '@angular/router'
import { catchError, EMPTY, Observable } from 'rxjs'
import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
import { NotificationsService } from '@geonetwork-ui/feature/notifications'
import { TranslateService } from '@ngx-translate/core'
import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface'

@Injectable({
providedIn: 'root',
})
export class DuplicateRecordResolver {
constructor(
private recordsRepository: RecordsRepositoryInterface,
private notificationsService: NotificationsService,
private translateService: TranslateService
) {}

resolve(
route: ActivatedRouteSnapshot
): Observable<[CatalogRecord, string, boolean]> {
return this.recordsRepository
.openRecordForDuplication(route.paramMap.get('uuid'))
.pipe(
catchError((error) => {
this.notificationsService.showNotification({
type: 'error',
title: this.translateService.instant(
'editor.record.loadError.title'
),
text: `${this.translateService.instant(
'editor.record.loadError.body'
)} ${error.message}`,
closeMessage: this.translateService.instant(
'editor.record.loadError.closeMessage'
),
})
return EMPTY
})
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ <h1 class="text-[16px] text-main font-title font-bold">
>
<gn-ui-results-table-container
(recordClick)="editRecord($event)"
(duplicateRecord)="duplicateRecord($event)"
></gn-ui-results-table-container>
<div class="px-5 py-5 flex justify-center gap-8 items-baseline">
<div class="grow">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const totalPages = 25
})
export class ResultsTableContainerComponent {
@Output() recordClick = new EventEmitter<CatalogRecord>()
@Output() duplicateRecord = new EventEmitter<CatalogRecord>()
}

@Component({
Expand Down Expand Up @@ -136,6 +137,19 @@ describe('RecordsListComponent', () => {
expect(router.navigate).toHaveBeenCalledWith(['/edit', 123])
})
})
describe('when asking for record duplication', () => {
const uniqueIdentifier = 123
const singleRecord = {
...DATASET_RECORDS[0],
uniqueIdentifier,
}
beforeEach(() => {
table.duplicateRecord.emit(singleRecord)
})
it('routes to record duplication', () => {
expect(router.navigate).toHaveBeenCalledWith(['/duplicate', 123])
})
})
describe('when click on pagination', () => {
beforeEach(() => {
pagination.newCurrentPageEvent.emit(3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export class RecordsListComponent {
this.router.navigate(['/edit', record.uniqueIdentifier])
}

duplicateRecord(record: CatalogRecord) {
this.router.navigate(['/duplicate', record.uniqueIdentifier])
}

showUsers() {
this.router.navigate(['/users/my-org'])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ <h1 class="text-[16px] text-main font-title font-bold" translate>
<gn-ui-results-table-container
class="text-[14px]"
(recordClick)="editRecord($event)"
(duplicateRecord)="duplicateRecord($event)"
></gn-ui-results-table-container>

<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 @@ -30,6 +30,7 @@ const totalPages = 25
})
export class ResultsTableContainerComponent {
@Output() recordClick = new EventEmitter<CatalogRecord>()
@Output() duplicateRecord = new EventEmitter<CatalogRecord>()
}

@Component({
Expand Down Expand Up @@ -152,6 +153,19 @@ describe('SearchRecordsComponent', () => {
expect(router.navigate).toHaveBeenCalledWith(['/edit', 123])
})
})
describe('when asking for record duplication', () => {
const uniqueIdentifier = 123
const singleRecord = {
...DATASET_RECORDS[0],
uniqueIdentifier,
}
beforeEach(() => {
table.duplicateRecord.emit(singleRecord)
})
it('routes to record duplication', () => {
expect(router.navigate).toHaveBeenCalledWith(['/duplicate', 123])
})
})
describe('when click on pagination', () => {
beforeEach(() => {
pagination.newCurrentPageEvent.emit(3)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export class SearchRecordsComponent {
this.router.navigate(['/edit', record.uniqueIdentifier])
}

duplicateRecord(record: CatalogRecord) {
this.router.navigate(['/duplicate', record.uniqueIdentifier])
}

createRecord() {
this.router.navigate(['/create'])
}
Expand Down
1 change: 1 addition & 0 deletions libs/api/metadata-converter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './lib/base.converter'
export * from './lib/iso19139'
export * from './lib/iso19115-3'
export * from './lib/find-converter'
Expand Down
48 changes: 48 additions & 0 deletions libs/api/repository/src/lib/gn4/gn4-repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,54 @@ describe('Gn4Repository', () => {
})
})
})
describe('openRecordForDuplication', () => {
let record: CatalogRecord
let recordSource: string
let savedOnce: boolean

const date = new Date('2024-07-11')
jest.useFakeTimers().setSystemTime(date)

beforeEach(async () => {
;(gn4RecordsApi.getRecordAs as jest.Mock).mockReturnValueOnce(
of(DATASET_RECORD_SIMPLE_AS_XML).pipe(map((xml) => ({ body: xml })))
)
;[record, recordSource, savedOnce] = await lastValueFrom(
repository.openRecordForDuplication('1234-5678')
)
})
it('calls the API to get the record as XML', () => {
expect(gn4RecordsApi.getRecordAs).toHaveBeenCalledWith(
'1234-5678',
undefined,
expect.anything(),
undefined,
undefined,
undefined,
expect.anything(),
expect.anything(),
undefined,
expect.anything()
)
})
it('parses the XML record into a native object, and updates the id and title', () => {
expect(record).toMatchObject({
uniqueIdentifier: `TEMP-ID-1720656000000`,
title:
'A very interesting dataset (un jeu de données très intéressant) (Copy)',
})
})
it('saves the duplicated record as draft', () => {
const hasDraft = repository.recordHasDraft(`TEMP-ID-1720656000000`)
expect(hasDraft).toBe(true)
})
it('tells the record it has not been saved yet', () => {
expect(savedOnce).toBe(false)
})
it('returns the record as serialized', () => {
expect(recordSource).toMatch(/<mdb:MD_Metadata/)
})
})
// note: we're using a simple record here otherwise there might be loss of information when converting
describe('saveRecord', () => {
let recordSource: string
Expand Down
20 changes: 20 additions & 0 deletions libs/api/repository/src/lib/gn4/gn4-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@geonetwork-ui/common/domain/model/search'
import { catchError, map, tap } from 'rxjs/operators'
import {
BaseConverter,
findConverterForDocument,
Gn4Converter,
Gn4SearchResults,
Expand Down Expand Up @@ -230,6 +231,25 @@ export class Gn4Repository implements RecordsRepositoryInterface {
)
}

openRecordForDuplication(
uniqueIdentifier: string
): Observable<[CatalogRecord, string, false] | null> {
return this.loadRecordAsXml(uniqueIdentifier).pipe(
switchMap(async (recordAsXml) => {
const converter = findConverterForDocument(recordAsXml)
const record = await converter.readRecord(recordAsXml)
record.uniqueIdentifier = `TEMP-ID-${Date.now()}`
record.title = `${record.title} (Copy)`
const xml = await converter.writeRecord(record, recordAsXml)
window.localStorage.setItem(
this.getLocalStorageKeyForRecord(record.uniqueIdentifier),
xml
)
return [record, xml, false] as [CatalogRecord, string, false]
})
)
}

private serializeRecordToXml(
record: CatalogRecord,
referenceRecordSource?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ export abstract class RecordsRepositoryInterface {
uniqueIdentifier: string
): Observable<[CatalogRecord, string, boolean] | null>

/**
* This emits once:
* - record object with a new unique identifier and suffixed title
* - serialized representation of the record as text
* - false, as the duplicated record is always a draft
* @param uniqueIdentifier
* @returns Observable<[CatalogRecord, string, false] | null>
*/
abstract openRecordForDuplication(
uniqueIdentifier: string
): Observable<[CatalogRecord, string, false] | null>

/**
* @param record
* @param referenceRecordSource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[selectedRecordsIdentifiers]="selectedRecords$ | async"
[sortOrder]="sortBy$ | async"
(recordClick)="handleRecordClick($event)"
(duplicateRecord)="handleDuplicateRecord($event)"
(recordsSelectedChange)="handleRecordsSelectedChange($event[0], $event[1])"
(sortByChange)="handleSortByChange($event[0], $event[1])"
></gn-ui-results-table>
Loading

0 comments on commit 613f731

Please sign in to comment.