diff --git a/conf/default.toml b/conf/default.toml index 1325a05793..df6e051e8e 100644 --- a/conf/default.toml +++ b/conf/default.toml @@ -48,7 +48,7 @@ proxy_path = "" [theme] primary_color = "#c82850" secondary_color = "#001638" -main_color = "#555" # All-purpose text color +main_color = "#555" # All-purpose text color background_color = "#fdfbff" # These optional parameters indicate which background should be used for the main header and the text color used diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts index 20d6b52f15..c6c47a90ba 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.converter.spec.ts @@ -1006,6 +1006,30 @@ describe('Gn4Converter', () => { }) }) + describe('feature catalog (fcats)', () => { + it('sets the featureCatalogIdentifier from the fcats', async () => { + const record = await service.readRecord({ + ...hit, + _source: { + ...hit._source, + related: { + fcats: [ + { + _source: { + uuid: 'related-metadata-with-fcats', + }, + }, + ], + }, + }, + }) + + expect(record.featureCatalogIdentifier).toEqual( + 'related-metadata-with-fcats' + ) + }) + }) + describe('full record', () => { it('builds a complete record object', async () => { const record = await service.readRecord( @@ -2363,6 +2387,7 @@ describe('Gn4Converter', () => { isPublishedToAll: true, id: '53583', favoriteCount: 0, + featureTypes: [], catalogUuid: 'metawal.wallonie.be', edit: true, }, @@ -2642,6 +2667,7 @@ describe('Gn4Converter', () => { extras: { catalogUuid: 'metawal.wallonie.be', favoriteCount: 0, + featureTypes: [], id: '1215', isOpenData: false, isPublishedToAll: true, diff --git a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts index c858149215..94a2682b27 100644 --- a/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +++ b/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts @@ -277,6 +277,32 @@ export class Gn4FieldMapper { }, output ), + featureTypes: (output, source) => + this.addExtra( + { + featureTypes: selectField(source, 'featureTypes'), + }, + output + ), + related: (output, source) => { + const fcatSource = selectField( + getFirstValue( + selectField( + selectField(source, 'related'), + 'fcats' + ) + ), + '_source' + ) + const featureCatalogIdentifier = selectField( + fcatSource, + 'uuid' + ) + return { + ...output, + ...(featureCatalogIdentifier && { featureCatalogIdentifier }), + } as CatalogRecord + }, isPublishedToAll: (output, source) => this.addExtra( { diff --git a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts index 3cbfce4b74..67daf4d0eb 100644 --- a/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +++ b/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts @@ -203,3 +203,7 @@ export interface Gn4Record { guestdownload?: boolean selected?: boolean } + +export interface Gn4RecordRelated { + fcats?: Gn4Record[] +} diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts index cfb0b56d1b..f724f13bb3 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.spec.ts @@ -18,9 +18,13 @@ import { datasetRecordsFixture, simpleDatasetRecordAsXmlFixture, simpleDatasetRecordFixture, + simpleDatasetRecordWithFcatsFixture, duplicateDatasetRecordAsXmlFixture, } from '@geonetwork-ui/common/fixtures' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { + CatalogRecord, + DatasetFeatureCatalog, +} from '@geonetwork-ui/common/domain/model/record' import { map } from 'rxjs/operators' import { HttpErrorResponse } from '@angular/common/http' import { @@ -53,7 +57,7 @@ class ElasticsearchServiceMock { } class SearchApiServiceMock { - search = jest.fn((bucket, relatedType, payload) => { + search(bucket, relatedType, payload) { const body = JSON.parse(payload) const count = body.size || 1234 const result: EsSearchResponse = { @@ -70,16 +74,16 @@ class SearchApiServiceMock { } } return of(result) - }) + } } class RecordsApiServiceMock { getRecordAs = jest.fn((uuid) => of(` - - ${uuid} - -`).pipe(map((xml) => ({ body: xml }))) + + ${uuid} + + `).pipe(map((xml) => ({ body: xml }))) ) insert = jest.fn(() => of({ @@ -253,6 +257,97 @@ describe('Gn4Repository', () => { }) }) }) + + describe('getFeatureCatalog', () => { + let catalog: DatasetFeatureCatalog + let metadata: CatalogRecord + const spySearch = jest.spyOn(SearchApiServiceMock.prototype, 'search') + + describe('when extras feature catalog is defined ', () => { + beforeEach(async () => { + jest.clearAllMocks() + metadata = datasetRecordsFixture()[0] + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) + }) + + it('should not call the feature catalog API (extras contain the answer) ', () => { + expect(spySearch).not.toHaveBeenCalled() + }) + + it('returns the feature catalog with mapped features', () => { + expect(catalog).toEqual({ + attributes: [ + { name: 'OBJECTID', title: 'Object identifier' }, + { name: 'Nom', title: 'Nom de la rue' }, + { name: 'Rue', title: '' }, + ], + }) + }) + }) + describe('when feature catalog exists, no extras defined', () => { + beforeEach(async () => { + metadata = simpleDatasetRecordWithFcatsFixture() + spySearch.mockReturnValue( + of({ hits: { hits: datasetRecordsFixture(), total: { value: 0 } } }) + ) + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) + }) + + afterEach(() => { + spySearch.mockRestore() + }) + + it('calls the API with correct parameters', () => { + expect(spySearch).toHaveBeenCalledWith( + 'bucket', + ['fcats'], + '{"uuids":["feature-catalog-identifier"]}' + ) + }) + + it('returns the attributes coming from the linked feature catalog', () => { + expect(catalog).toEqual({ + attributes: [ + { name: 'OBJECTID', title: 'Object identifier' }, + { name: 'Nom', title: 'Nom de la rue' }, + { name: 'Rue', title: '' }, + ], + }) + }) + + it('prevent looping', async () => { + metadata = { + ...simpleDatasetRecordWithFcatsFixture(), + ...{ featureCatalogIdentifier: 'my-dataset-with-fcats' }, + } + spySearch.mockReturnValue( + of({ + hits: { + hits: metadata, + total: { value: 0 }, + }, + }) + ) + repository.getRecord = jest.fn().mockReturnValue(of(metadata)) + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) + expect(catalog).toEqual(null) + }) + }) + + describe('when feature catalog does not exist, nor in extras', () => { + beforeEach(async () => { + metadata = { + ...simpleDatasetRecordFixture(), + extras: {}, + } + catalog = await lastValueFrom(repository.getFeatureCatalog(metadata)) + }) + it('returns null', () => { + expect(catalog).toEqual(null) + }) + }) + }) + describe('getSimilarRecords', () => { let results: CatalogRecord[] beforeEach(async () => { diff --git a/libs/api/repository/src/lib/gn4/gn4-repository.ts b/libs/api/repository/src/lib/gn4/gn4-repository.ts index 15f27fe0d4..8f5e7ba968 100644 --- a/libs/api/repository/src/lib/gn4/gn4-repository.ts +++ b/libs/api/repository/src/lib/gn4/gn4-repository.ts @@ -12,7 +12,10 @@ import { Iso19139Converter, } from '@geonetwork-ui/api/metadata-converter' import { PublicationVersionError } from '@geonetwork-ui/common/domain/model/error' -import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' +import { + CatalogRecord, + DatasetFeatureCatalog, +} from '@geonetwork-ui/common/domain/model/record' import { Aggregations, AggregationsParams, @@ -27,6 +30,7 @@ import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/reposit import { RecordsApiService, SearchApiService, + FeatureResponseApiModel, } from '@geonetwork-ui/data-access/gn4' import { combineLatest, @@ -124,7 +128,7 @@ export class Gn4Repository implements RecordsRepositoryInterface { return this.gn4SearchApi .search( 'bucket', - null, + ['fcats'], JSON.stringify( this.gn4SearchHelper.getMetadataByIdPayload(uniqueIdentifier) ) @@ -137,6 +141,39 @@ export class Gn4Repository implements RecordsRepositoryInterface { ) } + getFeatureCatalog( + record: CatalogRecord, + visited: Set = new Set() // prevent looping + ): Observable { + if ( + record.extras && + record.extras['featureTypes'] && + record.extras['featureTypes'][0]?.attributeTable && + Array.isArray(record.extras['featureTypes'][0].attributeTable) + ) { + return of({ + attributes: record.extras['featureTypes'][0]?.attributeTable?.map( + (attr) => ({ + name: attr.name, + title: attr.definition, + }) + ), + } as DatasetFeatureCatalog) + } + + const featureCatalogIdentifier = record.featureCatalogIdentifier + if (featureCatalogIdentifier && !visited.has(featureCatalogIdentifier)) { + visited.add(featureCatalogIdentifier) + return this.getRecord(featureCatalogIdentifier).pipe( + switchMap((record) => + record ? this.getFeatureCatalog(record, visited) : of(null) + ) + ) + } + + return of(null) + } + getSimilarRecords(similarTo: CatalogRecord): Observable { return this.gn4SearchApi .search( diff --git a/libs/common/domain/src/lib/model/record/metadata.model.ts b/libs/common/domain/src/lib/model/record/metadata.model.ts index 94b9c01b5b..d077ce29a3 100644 --- a/libs/common/domain/src/lib/model/record/metadata.model.ts +++ b/libs/common/domain/src/lib/model/record/metadata.model.ts @@ -124,6 +124,7 @@ export interface BaseRecord { extras?: Record landingPage?: URL updateFrequency?: UpdateFrequency + featureCatalogIdentifier?: string // information related to the resource (dataset, service) resourceIdentifier?: string @@ -226,7 +227,9 @@ export interface DatasetRecord extends BaseRecord { temporalExtents: Array spatialRepresentation?: SpatialRepresentationType } - +export type DatasetFeatureCatalog = { + attributes: Array<{ name: string; title: string }> +} export interface ServiceEndpoint { endpointUrl: URL protocol: string @@ -260,7 +263,7 @@ export type OnlineResource = DatasetOnlineResource | ServiceOnlineResource export type CatalogRecord = DatasetRecord | ReuseRecord | ServiceRecord -export type CatalogRecordKeys = - | keyof DatasetRecord - | keyof ReuseRecord - | keyof ServiceRecord +export type CatalogRecordKeys = Exclude< + keyof DatasetRecord | keyof ReuseRecord | keyof ServiceRecord, + 'featureCatalogIdentifier' +> diff --git a/libs/common/domain/src/lib/repository/records-repository.interface.ts b/libs/common/domain/src/lib/repository/records-repository.interface.ts index bfc85c5513..39c4d33b9c 100644 --- a/libs/common/domain/src/lib/repository/records-repository.interface.ts +++ b/libs/common/domain/src/lib/repository/records-repository.interface.ts @@ -6,12 +6,15 @@ import { SearchParams, SearchResults, } from '../model/search' -import { CatalogRecord } from '../model/record' +import { CatalogRecord, DatasetFeatureCatalog } from '../model/record' export abstract class RecordsRepositoryInterface { abstract search(params: SearchParams): Observable abstract getMatchesCount(filters: FieldFilters): Observable abstract getRecord(uniqueIdentifier: string): Observable + abstract getFeatureCatalog( + record: CatalogRecord + ): Observable abstract aggregate(params: AggregationsParams): Observable abstract getSimilarRecords( similarTo: CatalogRecord diff --git a/libs/common/fixtures/src/lib/records.fixtures.ts b/libs/common/fixtures/src/lib/records.fixtures.ts index ec77d7453b..ab165926ea 100644 --- a/libs/common/fixtures/src/lib/records.fixtures.ts +++ b/libs/common/fixtures/src/lib/records.fixtures.ts @@ -160,6 +160,38 @@ As such, **it is not very interesting at all.**`, extras: { isPublishedToAll: true, edit: true, + featureTypes: [ + { + attributeTable: [ + { + code: 'OBJECTID', + name: 'OBJECTID', + link: '', + definition: 'Object identifier', + type: 'OID', + }, + { + code: 'NOM', + name: 'Nom', + link: '', + definition: 'Nom de la rue', + type: 'String (48)', + }, + { + code: 'RUE', + name: 'Rue', + link: '', + definition: '', + type: 'String (50)', + }, + ], + code: '', + aliases: '', + typeName: "Catalogue d'attributs", + definition: '', + isAbstract: 'false', + }, + ], }, }, { @@ -312,6 +344,47 @@ export const simpleDatasetRecordFixture = (): DatasetRecord => ({ translations: {}, }) +export const simpleDatasetRecordWithFcatsFixture = (): DatasetRecord => ({ + uniqueIdentifier: 'my-dataset-with-fcats', + featureCatalogIdentifier: 'feature-catalog-identifier', + kind: 'dataset', + otherLanguages: [], + defaultLanguage: 'en', + recordUpdated: new Date('2022-02-01T14:12:00.000Z'), + resourceCreated: new Date('2022-09-01T12:18:19.000Z'), + resourceUpdated: new Date('2022-12-04T14:12:00.000Z'), + status: 'ongoing', + title: 'A very interesting dataset with a related feature catalog', + abstract: `This dataset has been established for testing purposes.`, + ownerOrganization: { name: 'MyOrganization', translations: {} }, + contacts: [ + { + email: 'bob@org.net', + position: 'developer', + organization: { name: 'MyOrganization', translations: {} }, + role: 'point_of_contact', + firstName: 'Bob', + lastName: 'TheGreat', + }, + ], + contactsForResource: [], + keywords: [], + topics: ['testData'], + licenses: [], + legalConstraints: [], + securityConstraints: [], + otherConstraints: [], + lineage: + 'This record was edited manually to test the feature catalog parsing', + spatialRepresentation: 'grid', + overviews: [], + spatialExtents: [], + temporalExtents: [], + onlineResources: [], + updateFrequency: { per: 'month', updatedTimes: 3 }, + translations: {}, +}) + export const simpleDatasetRecordAsXmlFixture = (): string => ` diff --git a/libs/feature/record/src/lib/state/mdview.actions.ts b/libs/feature/record/src/lib/state/mdview.actions.ts index 9cc84be989..8a8d5d19ab 100644 --- a/libs/feature/record/src/lib/state/mdview.actions.ts +++ b/libs/feature/record/src/lib/state/mdview.actions.ts @@ -2,6 +2,7 @@ import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/da import { createAction, props } from '@ngrx/store' import { CatalogRecord, + DatasetFeatureCatalog, UserFeedback, } from '@geonetwork-ui/common/domain/model/record' @@ -28,6 +29,21 @@ export const loadFullMetadataFailure = createAction( props<{ otherError?: string; notFound?: boolean }>() ) +export const loadFeatureCatalog = createAction( + "[Metadata view] Load metadata's feature catalog", + props<{ metadata: CatalogRecord }>() +) + +export const loadFeatureCatalogSuccess = createAction( + '[Metadata view] Load metadata feature catalog success', + props<{ datasetCatalog: DatasetFeatureCatalog }>() +) + +export const loadFeatureCatalogFailure = createAction( + '[Metadata view] Load metadata feature catalog failure', + props<{ otherError?: string; notFound?: boolean }>() +) + export const closeMetadata = createAction('[Metadata view] close') /* diff --git a/libs/feature/record/src/lib/state/mdview.effects.spec.ts b/libs/feature/record/src/lib/state/mdview.effects.spec.ts index cfd7f71d04..7432b9feb2 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.spec.ts @@ -262,4 +262,61 @@ describe('MdViewEffects', () => { }) }) }) + + describe('loadFeatureCatalog$', () => { + const featureCatalog = { + attributes: [{ name: 'test', title: 'Test' }], + } + + describe('when api success and feature catalog found', () => { + beforeEach(() => { + repository.getFeatureCatalog = jest.fn(() => of(featureCatalog)) + }) + it('should dispatch loadFeatureCatalogSuccess', () => { + actions = hot('-a-|', { + a: MdViewActions.loadFullMetadataSuccess({ full }), + }) + const expected = hot('-a-|', { + a: MdViewActions.loadFeatureCatalogSuccess({ + datasetCatalog: featureCatalog, + }), + }) + expect(effects.loadFeatureCatalog$).toBeObservable(expected) + }) + }) + + describe('when api success but no feature catalog found', () => { + beforeEach(() => { + repository.getFeatureCatalog = jest.fn(() => of(null)) + }) + it('should dispatch loadFeatureCatalogFailure with notFound', () => { + actions = hot('-a-|', { + a: MdViewActions.loadFullMetadataSuccess({ full }), + }) + const expected = hot('-a-|', { + a: MdViewActions.loadFeatureCatalogFailure({ notFound: true }), + }) + expect(effects.loadFeatureCatalog$).toBeObservable(expected) + }) + }) + + describe('when api fails', () => { + beforeEach(() => { + repository.getFeatureCatalog = jest.fn(() => + throwError(() => new Error('api error')) + ) + }) + it('should dispatch loadFeatureCatalogFailure with error', () => { + actions = hot('-a-|', { + a: MdViewActions.loadFullMetadataSuccess({ full }), + }) + const expected = hot('-(a|)', { + a: MdViewActions.loadFeatureCatalogFailure({ + otherError: 'api error', + }), + }) + expect(effects.loadFeatureCatalog$).toBeObservable(expected) + }) + }) + }) }) diff --git a/libs/feature/record/src/lib/state/mdview.effects.ts b/libs/feature/record/src/lib/state/mdview.effects.ts index cc97d7918c..44b12538ff 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.ts @@ -1,11 +1,10 @@ import { Injectable } from '@angular/core' import { Actions, createEffect, ofType } from '@ngrx/effects' import { exhaustMap, mergeMap, of } from 'rxjs' -import { catchError, map, switchMap } from 'rxjs/operators' +import { catchError, filter, map, switchMap, take } from 'rxjs/operators' import * as MdViewActions from './mdview.actions' import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface' import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' - @Injectable() export class MdViewEffects { constructor( @@ -33,6 +32,31 @@ export class MdViewEffects { ) ) + loadFeatureCatalog$ = createEffect(() => + this.actions$.pipe( + ofType(MdViewActions.loadFullMetadataSuccess), + filter(({ full }) => full !== undefined), + switchMap(({ full }) => this.recordsRepository.getFeatureCatalog(full)), + map((featureCatalog) => { + if (featureCatalog === null) { + return MdViewActions.loadFeatureCatalogFailure({ + notFound: true, + }) + } + return MdViewActions.loadFeatureCatalogSuccess({ + datasetCatalog: featureCatalog, + }) + }), + catchError((error) => + of( + MdViewActions.loadFeatureCatalogFailure({ + otherError: error.message, + }) + ) + ) + ) + ) + /* Related effects */ diff --git a/libs/feature/record/src/lib/state/mdview.facade.spec.ts b/libs/feature/record/src/lib/state/mdview.facade.spec.ts index a5df490a5a..bd434a69a2 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.spec.ts @@ -380,5 +380,16 @@ describe('MdViewFacade', () => { expect(result).toEqual(values.a) })) }) + describe('loadFeatureCatalog', () => { + it('dispatches a loadFeatureCatalog action', () => { + facade.loadFeatureCatalog(datasetRecordsFixture()[0]) + const expected = hot('a', { + a: MdViewActions.loadFeatureCatalog({ + metadata: datasetRecordsFixture()[0], + }), + }) + expect(store.scannedActions$).toBeObservable(expected) + }) + }) }) }) diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index bba7c3ea6e..0e64e0fd41 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -52,6 +52,11 @@ export class MdViewFacade { filter((md) => !!md) ) + featureCatalog$ = this.store.pipe( + select(MdViewSelectors.getFeatureCatalog), + filter((fc) => !!fc) + ) + isIncomplete$ = this.store.pipe( select(MdViewSelectors.getMetadataIsIncomplete), filter((incomplete) => incomplete !== null) @@ -204,4 +209,15 @@ export class MdViewFacade { loadUserFeedbacks(datasetUuid: string) { this.store.dispatch(MdViewActions.loadUserFeedbacks({ datasetUuid })) } + + /** + * FeatureCatalog + */ + loadFeatureCatalog(metadata: CatalogRecord) { + this.store.dispatch( + MdViewActions.loadFeatureCatalog({ + metadata, + }) + ) + } } diff --git a/libs/feature/record/src/lib/state/mdview.reducer.spec.ts b/libs/feature/record/src/lib/state/mdview.reducer.spec.ts index fee04f182f..5b24c1f535 100644 --- a/libs/feature/record/src/lib/state/mdview.reducer.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.reducer.spec.ts @@ -255,4 +255,80 @@ describe('metadataViewReducer', () => { }) }) }) + + describe('loadFeatureCatalog', () => { + let action + + beforeEach(() => { + action = MdViewActions.loadFeatureCatalog({ + metadata: datasetRecordsFixture()[0], + }) + }) + + it('should set loading state and clear errors', () => { + const state = reducer(initialMetadataViewState, action) + expect(state).toEqual({ + ...initialMetadataViewState, + featureCatalogError: null, + featureCatalogLoading: true, + }) + }) + }) + + describe('loadFeatureCatalogSuccess', () => { + let action + const mockDatasetCatalog = { + attributes: [ + { name: 'feature1', title: 'Feature 1' }, + { name: 'feature2', title: 'Feature 2' }, + ], + } + + beforeEach(() => { + action = MdViewActions.loadFeatureCatalogSuccess({ + datasetCatalog: mockDatasetCatalog, + }) + }) + + it('should store the feature catalog and set loading to false', () => { + const state = reducer( + { + ...initialMetadataViewState, + featureCatalogLoading: true, + }, + action + ) + expect(state).toEqual({ + ...initialMetadataViewState, + featureCatalog: mockDatasetCatalog, + featureCatalogLoading: false, + }) + }) + }) + + describe('loadFeatureCatalogFailure', () => { + let action + + beforeEach(() => { + action = MdViewActions.loadFeatureCatalogFailure({ + otherError: 'Some error', + notFound: true, + }) + }) + + it('should set error and set loading to false', () => { + const state = reducer( + { + ...initialMetadataViewState, + featureCatalogLoading: true, + }, + action + ) + expect(state).toEqual({ + ...initialMetadataViewState, + featureCatalogError: { otherError: 'Some error', notFound: true }, + featureCatalogLoading: false, + }) + }) + }) }) diff --git a/libs/feature/record/src/lib/state/mdview.reducer.ts b/libs/feature/record/src/lib/state/mdview.reducer.ts index e09622e089..9646226c71 100644 --- a/libs/feature/record/src/lib/state/mdview.reducer.ts +++ b/libs/feature/record/src/lib/state/mdview.reducer.ts @@ -3,6 +3,7 @@ import * as MetadataViewActions from './mdview.actions' import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model' import { CatalogRecord, + DatasetFeatureCatalog, UserFeedback, } from '@geonetwork-ui/common/domain/model/record' @@ -17,6 +18,9 @@ export interface MetadataViewState { allUserFeedbacksLoading: boolean addUserFeedbackLoading: boolean chartConfig?: DatavizConfigurationModel + featureCatalog?: DatasetFeatureCatalog + featureCatalogLoading: boolean + featureCatalogError: { notFound?: boolean; otherError?: string } | null } export const initialMetadataViewState: MetadataViewState = { @@ -24,6 +28,8 @@ export const initialMetadataViewState: MetadataViewState = { loadingFull: false, allUserFeedbacksLoading: false, addUserFeedbackLoading: false, + featureCatalogLoading: false, + featureCatalogError: null, } const metadataViewReducer = createReducer( @@ -105,6 +111,32 @@ const metadataViewReducer = createReducer( addUserFeedbackLoading: false, allUserFeedbacksLoading: false, }) + ), + + /** + * FeatureCatalog reducers + */ + + on(MetadataViewActions.loadFeatureCatalog, (state) => ({ + ...state, + featureCatalogError: null, + featureCatalogLoading: true, + })), + on( + MetadataViewActions.loadFeatureCatalogSuccess, + (state, { datasetCatalog }) => ({ + ...state, + featureCatalog: datasetCatalog, + featureCatalogLoading: false, + }) + ), + on( + MetadataViewActions.loadFeatureCatalogFailure, + (state, { otherError, notFound }) => ({ + ...state, + featureCatalogError: { otherError, notFound }, + featureCatalogLoading: false, + }) ) ) diff --git a/libs/feature/record/src/lib/state/mdview.selectors.spec.ts b/libs/feature/record/src/lib/state/mdview.selectors.spec.ts index 23524200d6..3085ded92d 100644 --- a/libs/feature/record/src/lib/state/mdview.selectors.spec.ts +++ b/libs/feature/record/src/lib/state/mdview.selectors.spec.ts @@ -25,6 +25,20 @@ describe('MdView Selectors', () => { }, loadingFull: false, error: null, + featureCatalog: { + features: [ + { + name: 'feature_1', + title: 'Feature 1', + }, + { + name: 'feature_2', + title: 'Feature 2', + }, + ], + }, + featureCatalogLoading: false, + featureCatalogError: null, } }) @@ -41,6 +55,8 @@ describe('MdView Selectors', () => { error: null, allUserFeedbacksLoading: false, addUserFeedbackLoading: false, + featureCatalogLoading: false, + featureCatalogError: null, }) expect(results).toBe(null) }) @@ -73,6 +89,8 @@ describe('MdView Selectors', () => { error: null, allUserFeedbacksLoading: false, addUserFeedbackLoading: false, + featureCatalogLoading: false, + featureCatalogError: null, }) expect(results).toBe(null) }) @@ -108,6 +126,8 @@ describe('MdView Selectors', () => { error: null, allUserFeedbacksLoading: false, addUserFeedbackLoading: false, + featureCatalogLoading: false, + featureCatalogError: null, }) expect(results).toBe(null) }) @@ -132,5 +152,40 @@ describe('MdView Selectors', () => { expect(results).toEqual([chartConfigMock]) }) }) + + describe('getFeatureCatalog', () => { + it('returns the feature catalog', () => { + const expectedFeatures = { + features: [ + { + name: 'feature_1', + title: 'Feature 1', + }, + { + name: 'feature_2', + title: 'Feature 2', + }, + ], + } + const results = MdViewSelectors.getFeatureCatalog.projector(state) + expect(results).toStrictEqual(expectedFeatures) + }) + }) + + describe('getFeatureCatalogIsLoading', () => { + it('returns false if not loading', () => { + const results = + MdViewSelectors.getFeatureCatalogIsLoading.projector(state) + expect(results).toBe(false) + }) + + it('returns true if loading', () => { + const results = MdViewSelectors.getFeatureCatalogIsLoading.projector({ + ...state, + featureCatalogLoading: true, + }) + expect(results).toBe(true) + }) + }) }) }) diff --git a/libs/feature/record/src/lib/state/mdview.selectors.ts b/libs/feature/record/src/lib/state/mdview.selectors.ts index bb035e12a7..9b30a8582c 100644 --- a/libs/feature/record/src/lib/state/mdview.selectors.ts +++ b/libs/feature/record/src/lib/state/mdview.selectors.ts @@ -64,3 +64,15 @@ export const getAddUserFeedbacksLoading = createSelector( getMdViewState, (state: MetadataViewState) => state.addUserFeedbackLoading ) + +/* + Feature Catalog Selectors +*/ +export const getFeatureCatalog = createSelector( + getMdViewState, + (state: MetadataViewState) => state.featureCatalog +) +export const getFeatureCatalogIsLoading = createSelector( + getMdViewState, + (state: MetadataViewState) => state.featureCatalogLoading +)