@@ -32,7 +32,7 @@
@@ -40,7 +40,7 @@
record.metadata.quality
@@ -138,7 +138,7 @@
@@ -148,14 +148,22 @@
+
mapLinks?.length > 0 || geoDataLinks?.length > 0
)
)
+
displayData$ = combineLatest([
- this.facade.dataLinks$,
- this.facade.geoDataLinks$,
+ this.metadataViewFacade.dataLinks$,
+ this.metadataViewFacade.geoDataLinks$,
]).pipe(
map(
([dataLinks, geoDataLinks]) =>
dataLinks?.length > 0 || geoDataLinks?.length > 0
)
)
- displayDownload$ = this.facade.downloadLinks$.pipe(
+
+ displayDownload$ = this.metadataViewFacade.downloadLinks$.pipe(
+ map((links) => links?.length > 0)
+ )
+ displayApi$ = this.metadataViewFacade.apiLinks$.pipe(
map((links) => links?.length > 0)
)
- displayApi$ = this.facade.apiLinks$.pipe(map((links) => links?.length > 0))
- displayOtherLinks = this.facade.otherLinks$.pipe(
+
+ displayOtherLinks = this.metadataViewFacade.otherLinks$.pipe(
map((links) => links?.length > 0)
)
- displayRelated$ = this.facade.related$.pipe(
+ displayRelated$ = this.metadataViewFacade.related$.pipe(
map((records) => records?.length > 0)
)
- sourceLabel$ = this.facade.metadata$.pipe(
+ organisationName$ = this.metadataViewFacade.metadata$.pipe(
+ map((record) => record?.ownerOrganization?.name),
+ filter(Boolean)
+ )
+
+ metadataUuid$ = this.metadataViewFacade.metadata$.pipe(
+ map((record) => record?.uniqueIdentifier),
+ filter(Boolean)
+ )
+
+ sourceLabel$ = this.metadataViewFacade.metadata$.pipe(
map((record) => record?.extras?.catalogUuid as string),
filter((uuid) => !!uuid),
mergeMap((uuid) => this.sourceService.getSourceLabel(uuid))
)
errorTypes = ErrorType
+
selectedTabIndex$ = new BehaviorSubject(0)
- thumbnailUrl$ = this.facade.metadata$.pipe(
+ thumbnailUrl$ = this.metadataViewFacade.metadata$.pipe(
map((metadata) => {
// in order to differentiate between metadata not loaded yet
// and url not defined
@@ -74,7 +90,7 @@ export class RecordMetadataComponent {
showOverlay = true
constructor(
- public facade: MdViewFacade,
+ public metadataViewFacade: MdViewFacade,
private searchService: SearchService,
private sourceService: SourcesService,
private orgsService: OrganizationsServiceInterface
@@ -90,6 +106,7 @@ export class RecordMetadataComponent {
onInfoKeywordClick(keyword: Keyword) {
this.searchService.updateFilters({ any: keyword.label })
}
+
onOrganizationClick(org: Organization) {
this.orgsService
.getFiltersForOrgs([org])
diff --git a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.css b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.html b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.html
new file mode 100644
index 0000000000..29a7f9c157
--- /dev/null
+++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.html
@@ -0,0 +1,101 @@
+
+
+ record.metadata.userFeedbacks
+
+
+ 1"
+ [title]="'record.metadata.userFeedbacks.sortSelector.label' | translate"
+ (selectValue)="changeSort($event)"
+ [choices]="sortingStrategyList"
+ [selected]="selectedSortingStrategy$ | async"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.spec.ts b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.spec.ts
new file mode 100644
index 0000000000..2ccbb630f3
--- /dev/null
+++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.spec.ts
@@ -0,0 +1,160 @@
+import {
+ ComponentFixture,
+ fakeAsync,
+ TestBed,
+ tick,
+} from '@angular/core/testing'
+import { RecordUserFeedbacksComponent } from './record-user-feedbacks.component'
+import { TranslateModule } from '@ngx-translate/core'
+import { MdViewFacade } from '@geonetwork-ui/feature/record'
+import { BehaviorSubject, of, Subject } from 'rxjs'
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ NO_ERRORS_SCHEMA,
+} from '@angular/core'
+import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface'
+import {
+ DATASET_RECORDS,
+ SOME_USER_FEEDBACKS,
+ USER_FIXTURE,
+} from '@geonetwork-ui/common/fixtures'
+import {
+ UserFeedback,
+ UserFeedbackViewModel,
+} from '@geonetwork-ui/common/domain/model/record'
+import { Gn4PlatformMapper } from '@geonetwork-ui/api/repository'
+
+describe('RelatedRecordsComponent', () => {
+ const allUserFeedbacks = SOME_USER_FEEDBACKS
+ let mockDestroy$: Subject
+
+ const activeUser = USER_FIXTURE()
+
+ const mdViewFacadeMock: Partial = {
+ isAllUserFeedbackLoading$: new BehaviorSubject(false),
+ isAddUserFeedbackLoading$: new BehaviorSubject(false),
+ loadUserFeedbacks: jest.fn(),
+ userFeedbacks$: of(allUserFeedbacks),
+ addUserFeedback: jest.fn(),
+ }
+
+ const gn4PlatformMapperMock: Partial = {
+ createUserFeedbackViewModel: (baseUserFeedback) => {
+ return Promise.resolve({
+ ...baseUserFeedback,
+ avatarUrl: 'someAvatarUrl',
+ } as UserFeedbackViewModel)
+ },
+ }
+
+ const changeDetectorRefMock: Partial = {
+ markForCheck: jest.fn(),
+ }
+
+ const platformServiceInterfaceMock: Partial = {
+ getUserFeedbacks: jest.fn(),
+ getMe: jest.fn(() => new BehaviorSubject(activeUser)),
+ }
+
+ let component: RecordUserFeedbacksComponent
+ let fixture: ComponentFixture
+
+ beforeEach(async () => {
+ mockDestroy$ = new Subject()
+
+ await TestBed.configureTestingModule({
+ declarations: [RecordUserFeedbacksComponent],
+ imports: [TranslateModule.forRoot()],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ {
+ provide: MdViewFacade,
+ useValue: mdViewFacadeMock,
+ },
+ {
+ provide: ChangeDetectorRef,
+ useValue: changeDetectorRefMock,
+ },
+ {
+ provide: PlatformServiceInterface,
+ useValue: platformServiceInterfaceMock,
+ },
+ {
+ provide: Gn4PlatformMapper,
+ useValue: gn4PlatformMapperMock,
+ },
+ ],
+ })
+ .overrideComponent(RecordUserFeedbacksComponent, {
+ set: {
+ changeDetection: ChangeDetectionStrategy.Default,
+ },
+ })
+ .compileComponents()
+
+ fixture = TestBed.createComponent(RecordUserFeedbacksComponent)
+ component = fixture.componentInstance
+
+ component.destroy$ = mockDestroy$
+ component.metadataUuid = DATASET_RECORDS[0].uniqueIdentifier
+
+ fixture.detectChanges()
+ })
+
+ afterEach(() => {
+ mockDestroy$.next()
+ mockDestroy$.complete()
+ })
+
+ it('should create', () => {
+ fixture.detectChanges()
+ expect(component).toBeTruthy()
+ })
+
+ describe('ngOnInit()', () => {
+ it('should load user feedbacks', () => {
+ component.ngOnInit()
+ expect(mdViewFacadeMock.loadUserFeedbacks).toHaveBeenCalledWith(
+ DATASET_RECORDS[0].uniqueIdentifier
+ )
+ })
+ it('should set active user', fakeAsync(() => {
+ component.ngOnInit()
+ tick()
+ expect(component.activeUser).toEqual(USER_FIXTURE())
+ }))
+ it('should fetch user feedbacks and sort them correctly', async () => {
+ component.ngOnInit()
+ await fixture.whenStable()
+ expect(component.userFeedbacksParents.length).toBe(4)
+ expect(
+ component.userFeedBacksAnswers.get(SOME_USER_FEEDBACKS[0].uuid).length
+ ).toBe(2)
+ })
+ })
+
+ describe('publishNewComment()', () => {
+ it('should publish the new userFeedback', () => {
+ const expectedNewUserFeedback: UserFeedback = {
+ uuid: undefined,
+ comment: 'TEST',
+ metadataUUID: 'accroche_velos',
+ parentUuid: null,
+ published: true,
+ date: expect.any(Date),
+ authorUserId: activeUser.id,
+ authorEmail: activeUser.email,
+ authorName: `${activeUser.name} ${activeUser.surname}`,
+ }
+
+ component.newComment = 'TEST'
+ component.metadataUuid = 'accroche_velos'
+ fixture.detectChanges()
+ component.publishNewComment()
+ expect(mdViewFacadeMock.addUserFeedback).toHaveBeenCalledWith(
+ expectedNewUserFeedback
+ )
+ })
+ })
+})
diff --git a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.ts b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.ts
new file mode 100644
index 0000000000..7561f56215
--- /dev/null
+++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.ts
@@ -0,0 +1,229 @@
+import {
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ Input,
+ OnDestroy,
+ OnInit,
+} from '@angular/core'
+import { filter, switchMap, takeUntil } from 'rxjs/operators'
+import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'
+import {
+ UserFeedback,
+ UserFeedbackViewModel,
+} from '@geonetwork-ui/common/domain/model/record'
+import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface'
+import { UserModel } from '@geonetwork-ui/common/domain/model/user'
+import { DropdownChoice } from '@geonetwork-ui/ui/inputs'
+import { MdViewFacade } from '@geonetwork-ui/feature/record'
+import { TranslateService } from '@ngx-translate/core'
+import { AuthService, Gn4PlatformMapper } from '@geonetwork-ui/api/repository'
+import { UserApiModel } from '@geonetwork-ui/data-access/gn4'
+
+type UserFeedbackSortingFunction = (
+ userFeedbackA: UserFeedback,
+ userFeedbackB: UserFeedback
+) => number
+
+@Component({
+ selector: 'datahub-record-user-feedbacks',
+ templateUrl: './record-user-feedbacks.component.html',
+ styleUrls: ['./record-user-feedbacks.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class RecordUserFeedbacksComponent implements OnInit, OnDestroy {
+ @Input() organisationName$: Observable
+ @Input() metadataUuid: string
+
+ destroy$ = new Subject()
+
+ userFeedbacksParents: UserFeedbackViewModel[] = []
+ userFeedBacksAnswers: Map = new Map()
+
+ newComment = ''
+
+ isNewCommentEmpty = true
+
+ activeUser$: Observable
+ activeUser?: UserModel
+ isActiveUserMetadaEditor = false
+
+ loginUrl = this.authService.loginUrl
+
+ sortingStrategyList: Array = [
+ {
+ value: this.sortByDateFromNewestToOldest,
+ label: this.translate.instant(
+ 'record.metadata.userFeedbacks.sortSelector.choices.newestFirst'
+ ),
+ },
+ {
+ value: this.sortByDateFromOldestToNewest,
+ label: this.translate.instant(
+ 'record.metadata.userFeedbacks.sortSelector.choices.oldestFirst'
+ ),
+ },
+ ]
+
+ selectedSortingStrategy$ = new BehaviorSubject(
+ this.sortByDateFromNewestToOldest
+ )
+
+ isAllUserFeedbackLoading = false
+ isAddUserFeedbackLoading = false
+
+ constructor(
+ private readonly translate: TranslateService,
+ private readonly authService: AuthService,
+ private readonly metadataViewFacade: MdViewFacade,
+ private readonly cdr: ChangeDetectorRef,
+ private readonly mapper: Gn4PlatformMapper,
+ private readonly platformServiceInterface: PlatformServiceInterface
+ ) {
+ this.activeUser$ = this.platformServiceInterface.getMe()
+ }
+
+ ngOnInit(): void {
+ this.metadataViewFacade.isAllUserFeedbackLoading$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((isLoading) => (this.isAllUserFeedbackLoading = isLoading))
+
+ this.metadataViewFacade.isAddUserFeedbackLoading$
+ .pipe(takeUntil(this.destroy$))
+ .subscribe((isLoading) => (this.isAddUserFeedbackLoading = isLoading))
+
+ this.metadataViewFacade.loadUserFeedbacks(this.metadataUuid)
+
+ this.activeUser$.pipe(takeUntil(this.destroy$)).subscribe((user) => {
+ this.activeUser = user
+ this.isActiveUserMetadaEditor = [
+ 'Administrator',
+ 'UserAdmin',
+ 'Reviewer',
+ 'Editor',
+ ].includes(user?.profile)
+ })
+
+ combineLatest([
+ this.metadataViewFacade.userFeedbacks$,
+ this.selectedSortingStrategy$,
+ this.activeUser$,
+ ])
+ .pipe(
+ filter(([userFeedbacks]) => Boolean(userFeedbacks)),
+ takeUntil(this.destroy$),
+ switchMap(
+ async ([userFeedbacks, selectedSortingStrategy, activeUser]) => {
+ this.activeUser = activeUser
+
+ const userFeedbacksParents = userFeedbacks
+ .filter((feedback) => !feedback.parentUuid)
+ .sort(selectedSortingStrategy)
+
+ const userFeedbacksAnswers = userFeedbacks
+ .filter((feedback) => feedback.parentUuid)
+ .sort(this.sortByDateFromOldestToNewest)
+
+ const userFeedbacksParentsViewModels = await Promise.all(
+ userFeedbacksParents.map((feedback) =>
+ this.mapper.createUserFeedbackViewModel(feedback)
+ )
+ )
+
+ const userFeedbacksAnswersViewModels = await Promise.all(
+ userFeedbacksAnswers.map((feedback) =>
+ this.mapper.createUserFeedbackViewModel(feedback)
+ )
+ )
+
+ const userFeedBacksAnswersMap = new Map()
+ userFeedbacksAnswersViewModels.forEach(
+ (userFeedbackAnswerViewModel) => {
+ const parentUuid = userFeedbackAnswerViewModel.parentUuid
+ if (userFeedBacksAnswersMap.has(parentUuid)) {
+ userFeedBacksAnswersMap
+ .get(parentUuid)
+ .push(userFeedbackAnswerViewModel)
+ } else {
+ userFeedBacksAnswersMap.set(parentUuid, [
+ userFeedbackAnswerViewModel,
+ ])
+ }
+ }
+ )
+
+ return {
+ parentsViewModels: userFeedbacksParentsViewModels,
+ answersMap: userFeedBacksAnswersMap,
+ }
+ }
+ )
+ )
+ .subscribe({
+ next: ({ parentsViewModels, answersMap }) => {
+ this.userFeedbacksParents = parentsViewModels
+ this.userFeedBacksAnswers = answersMap
+ this.cdr.markForCheck()
+ },
+ error: (err) => {
+ console.error('Error processing feedback', err)
+ },
+ })
+ }
+
+ onNewCommentValueChange() {
+ this.isNewCommentEmpty = this.newComment.length === 0
+ }
+
+ onNewUserFeedbackAnswer(newUserFeedback: UserFeedbackViewModel) {
+ const userFeedBack = this.mapper.userFeedbacksFromApi(newUserFeedback)
+ this.newUserFeedback(userFeedBack)
+ }
+
+ publishNewComment() {
+ if (this.newComment.trim() === '') return
+
+ const newUserFeedback: UserFeedback = {
+ uuid: undefined,
+ comment: this.newComment,
+ metadataUUID: this.metadataUuid,
+ parentUuid: null,
+ published: true,
+ date: new Date(),
+ authorUserId: this.activeUser?.id,
+ authorEmail: this.activeUser?.email,
+ authorName: `${this.activeUser?.name} ${this.activeUser?.surname}`,
+ }
+
+ this.newUserFeedback(newUserFeedback)
+ this.newComment = ''
+ this.onNewCommentValueChange()
+ }
+
+ changeSort(selectedSortingStrategy: UserFeedbackSortingFunction) {
+ this.selectedSortingStrategy$.next(selectedSortingStrategy)
+ }
+
+ private newUserFeedback(newUserFeedback: UserFeedback) {
+ this.metadataViewFacade.addUserFeedback(newUserFeedback)
+ }
+
+ private sortByDateFromNewestToOldest(
+ userFeedbackA: UserFeedback,
+ userFeedbackB: UserFeedback
+ ): number {
+ return userFeedbackB.date.getTime() - userFeedbackA.date.getTime()
+ }
+
+ private sortByDateFromOldestToNewest(
+ userFeedbackA: UserFeedback,
+ userFeedbackB: UserFeedback
+ ): number {
+ return userFeedbackA.date.getTime() - userFeedbackB.date.getTime()
+ }
+
+ ngOnDestroy(): void {
+ this.destroy$.next()
+ this.destroy$.complete()
+ }
+}
diff --git a/libs/api/repository/src/lib/gn4/auth/avatar.service.interface.ts b/libs/api/repository/src/lib/gn4/auth/avatar.service.interface.ts
index 2ef85b8dc6..fefe043c16 100644
--- a/libs/api/repository/src/lib/gn4/auth/avatar.service.interface.ts
+++ b/libs/api/repository/src/lib/gn4/auth/avatar.service.interface.ts
@@ -3,4 +3,5 @@ import { Observable } from 'rxjs'
export abstract class AvatarServiceInterface {
public abstract getPlaceholder(): Observable
public abstract getProfileIcon(...args): Observable
+ public abstract getProfileIconUrl(userId: string): Promise
}
diff --git a/libs/api/repository/src/lib/gn4/auth/gravatar.service.spec.ts b/libs/api/repository/src/lib/gn4/auth/gravatar.service.spec.ts
index 6f31600dd7..b3acd9c750 100644
--- a/libs/api/repository/src/lib/gn4/auth/gravatar.service.spec.ts
+++ b/libs/api/repository/src/lib/gn4/auth/gravatar.service.spec.ts
@@ -47,4 +47,15 @@ describe('GravatarService', () => {
expect(placeholder).toEqual('https://www.gravatar.com/avatar/?d=mp')
})
})
+ describe('#getProfileIconUrl', () => {
+ it('returns url with identicon value, without hash', async () => {
+ const placeholder = await service.getProfileIconUrl('12345')
+ expect(placeholder).toEqual('https://www.gravatar.com/avatar/12345?d=404')
+ })
+ it('returns placeholder to be mp if no identicon value', async () => {
+ settingsService.identicon$.next('')
+ const placeholder = await service.getProfileIconUrl('12345')
+ expect(placeholder).toEqual('https://www.gravatar.com/avatar/12345?d=mp')
+ })
+ })
})
diff --git a/libs/api/repository/src/lib/gn4/auth/gravatar.service.ts b/libs/api/repository/src/lib/gn4/auth/gravatar.service.ts
index ce02bddb68..fcd615547b 100644
--- a/libs/api/repository/src/lib/gn4/auth/gravatar.service.ts
+++ b/libs/api/repository/src/lib/gn4/auth/gravatar.service.ts
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'
import { AvatarServiceInterface } from './avatar.service.interface'
import { Gn4SettingsService } from '../settings/gn4-settings.service'
import { map } from 'rxjs/operators'
-import { Observable } from 'rxjs'
+import { firstValueFrom, Observable } from 'rxjs'
@Injectable({
providedIn: 'root',
@@ -14,6 +14,7 @@ export class GravatarService implements AvatarServiceInterface {
private readonly identicon$ = this.gn4SettingsService.identicon$.pipe(
map((identicon) => identicon?.replace('gravatar:', ''))
)
+
constructor(private gn4SettingsService: Gn4SettingsService) {}
getPlaceholder(): Observable {
@@ -26,4 +27,14 @@ export class GravatarService implements AvatarServiceInterface {
map((identicon) => `${this.GRAVATAR_URL}${hash}?d=${identicon}`)
)
}
+
+ async getProfileIconUrl(userId: string) {
+ let iconUrl = ''
+ try {
+ iconUrl = await firstValueFrom(this.getProfileIcon(userId))
+ } catch (error) {
+ return ''
+ }
+ return iconUrl
+ }
}
diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.ts
index fdd0682d23..db5a54dfa1 100644
--- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.ts
+++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.mapper.ts
@@ -1,17 +1,23 @@
import {
MeResponseApiModel,
UserApiModel,
+ UserFeedbackDTOApiModel,
} from '@geonetwork-ui/data-access/gn4'
import { UserModel } from '@geonetwork-ui/common/domain/model/user/user.model'
import { Injectable } from '@angular/core'
-import { AvatarServiceInterface } from '../auth/avatar.service.interface'
+import { AvatarServiceInterface } from '../auth'
import { map } from 'rxjs/operators'
import { Observable, of } from 'rxjs'
import { ThesaurusModel } from '@geonetwork-ui/common/domain/model/thesaurus/thesaurus.model'
+import {
+ UserFeedback,
+ UserFeedbackViewModel,
+} from '@geonetwork-ui/common/domain/model/record'
@Injectable()
export class Gn4PlatformMapper {
constructor(private avatarService: AvatarServiceInterface) {}
+
userFromMeApi(apiUser: MeResponseApiModel): Observable {
if (!apiUser) return of(null)
const {
@@ -28,6 +34,7 @@ export class Gn4PlatformMapper {
.getProfileIcon(hash)
.pipe(map((profileIcon) => ({ ...user, profileIcon } as UserModel)))
}
+
userFromApi(apiUser: UserApiModel): UserModel {
if (!apiUser) return null
const {
@@ -65,4 +72,47 @@ export class Gn4PlatformMapper {
}
})
}
+
+ userFeedbacksFromApi(userFeedback: any): UserFeedback {
+ return {
+ uuid: userFeedback.uuid,
+ metadataUUID: userFeedback.metadataUUID,
+ comment: userFeedback.comment,
+ authorUserId: userFeedback.authorUserId.toString(),
+ authorName: userFeedback.authorName,
+ authorEmail: userFeedback.authorEmail,
+ published: userFeedback.published,
+ parentUuid: userFeedback.parentUuid ?? undefined,
+ date: new Date(userFeedback.date),
+ }
+ }
+
+ userFeedbacksToApi(
+ userFeedback: UserFeedback
+ ): Partial {
+ return {
+ uuid: userFeedback.uuid,
+ metadataUUID: userFeedback.metadataUUID,
+ comment: userFeedback.comment,
+ authorUserId: Number.parseInt(userFeedback.authorUserId),
+ authorName: userFeedback.authorName,
+ authorEmail: userFeedback.authorEmail,
+ published: userFeedback.published,
+ parentUuid: userFeedback.parentUuid,
+ date: userFeedback.date.getTime().toString(),
+ }
+ }
+
+ async createUserFeedbackViewModel(
+ baseUserFeedback: UserFeedback
+ ): Promise {
+ const userAvatarUrl = await this.avatarService.getProfileIconUrl(
+ baseUserFeedback.authorUserId?.toString()
+ )
+
+ return {
+ ...baseUserFeedback,
+ avatarUrl: userAvatarUrl,
+ }
+ }
}
diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts
index beb6438345..e062a7202a 100644
--- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts
+++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.spec.ts
@@ -3,14 +3,20 @@ import {
RegistriesApiService,
SiteApiService,
ToolsApiService,
+ UserfeedbackApiService,
+ UserFeedbackDTOApiModel,
UsersApiService,
} from '@geonetwork-ui/data-access/gn4'
import { TestBed } from '@angular/core/testing'
import { Gn4PlatformService } from './gn4-platform.service'
-import { firstValueFrom, lastValueFrom, of, Subject } from 'rxjs'
+import { firstValueFrom, lastValueFrom, of, Subject, throwError } from 'rxjs'
import { AvatarServiceInterface } from '../auth/avatar.service.interface'
import { Gn4PlatformMapper } from './gn4-platform.mapper'
import { LangService } from '@geonetwork-ui/util/i18n'
+import {
+ A_USER_FEEDBACK,
+ SOME_USER_FEEDBACKS,
+} from '@geonetwork-ui/common/fixtures'
let geonetworkVersion: string
@@ -122,11 +128,18 @@ class LangServiceMock {
iso3 = 'fre'
}
+class UserfeedbackApiServiceMock {
+ getUserComments = jest.fn(() => of(SOME_USER_FEEDBACKS))
+
+ newUserFeedback = jest.fn(() => of(undefined))
+}
+
describe('Gn4PlatformService', () => {
let service: Gn4PlatformService
let meApiService: MeApiService
let toolsApiService: ToolsApiService
let registriesApiService: RegistriesApiService
+ let userFeedbackApiService: UserfeedbackApiServiceMock
beforeEach(() => {
TestBed.configureTestingModule({
@@ -161,12 +174,17 @@ describe('Gn4PlatformService', () => {
provide: LangService,
useClass: LangServiceMock,
},
+ {
+ provide: UserfeedbackApiService,
+ useClass: UserfeedbackApiServiceMock,
+ },
],
})
service = TestBed.inject(Gn4PlatformService)
meApiService = TestBed.inject(MeApiService)
toolsApiService = TestBed.inject(ToolsApiService)
registriesApiService = TestBed.inject(RegistriesApiService)
+ userFeedbackApiService = TestBed.inject(UserfeedbackApiService as any)
})
it('creates', () => {
@@ -354,5 +372,60 @@ describe('Gn4PlatformService', () => {
])
})
})
+ describe('getUserFeedbacks', () => {
+ it('should call getUserComments with correct UUID and map results', (done) => {
+ const mockUuid = '1234'
+ const mockFeedbacks = SOME_USER_FEEDBACKS
+
+ service.getUserFeedbacks(mockUuid).subscribe({
+ next: (results) => {
+ expect(results).toEqual(mockFeedbacks)
+ expect(userFeedbackApiService.getUserComments).toHaveBeenCalledWith(
+ mockUuid
+ )
+ done()
+ },
+ error: done,
+ })
+ })
+
+ it('should handle errors', (done) => {
+ const mockUuid = '1234'
+ const errorResponse = new Error('Failed to fetch')
+ userFeedbackApiService.getUserComments.mockReturnValue(
+ throwError(() => errorResponse)
+ )
+
+ service.getUserFeedbacks(mockUuid).subscribe({
+ next: () => {
+ done('Expected error, but got success')
+ },
+ error: (error) => {
+ expect(error).toBe(errorResponse)
+ done()
+ },
+ })
+ })
+ })
+
+ describe('postUserFeedbacks', () => {
+ it('should process and post user feedbacks correctly', (done) => {
+ const expected: UserFeedbackDTOApiModel = {
+ ...A_USER_FEEDBACK,
+ authorUserId: expect.any(Number),
+ date: expect.any(String),
+ }
+
+ service.postUserFeedbacks(A_USER_FEEDBACK).subscribe({
+ next: () => {
+ expect(userFeedbackApiService.newUserFeedback).toHaveBeenCalledWith(
+ expected
+ )
+ done()
+ },
+ error: done,
+ })
+ })
+ })
})
})
diff --git a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts
index b7dff952ba..4c71fb3691 100644
--- a/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts
+++ b/libs/api/repository/src/lib/gn4/platform/gn4-platform.service.ts
@@ -6,11 +6,15 @@ import {
RegistriesApiService,
SiteApiService,
ToolsApiService,
+ UserfeedbackApiService,
UsersApiService,
} from '@geonetwork-ui/data-access/gn4'
import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface'
import { UserModel } from '@geonetwork-ui/common/domain/model/user/user.model'
-import { Organization } from '@geonetwork-ui/common/domain/model/record'
+import {
+ Organization,
+ UserFeedback,
+} from '@geonetwork-ui/common/domain/model/record'
import { Gn4PlatformMapper } from './gn4-platform.mapper'
import { ltr } from 'semver'
import { ThesaurusModel } from '@geonetwork-ui/common/domain/model/thesaurus/thesaurus.model'
@@ -21,9 +25,9 @@ const minApiVersion = '4.2.2'
@Injectable()
export class Gn4PlatformService implements PlatformServiceInterface {
private readonly type = 'GeoNetwork'
- private me$: Observable
- private users$: Observable
- private isAnonymous$: Observable
+ private readonly me$: Observable
+ private readonly users$: Observable
+ private readonly isUserAnonymous$: Observable
private keyTranslations$ = this.toolsApiService
.getTranslationsPackage1('gnui')
@@ -65,13 +69,18 @@ export class Gn4PlatformService implements PlatformServiceInterface {
private mapper: Gn4PlatformMapper,
private toolsApiService: ToolsApiService,
private registriesApiService: RegistriesApiService,
- private langService: LangService
+ private langService: LangService,
+ private userfeedbackApiService: UserfeedbackApiService
) {
this.me$ = this.meApi.getMe().pipe(
switchMap((apiUser) => this.mapper.userFromMeApi(apiUser)),
shareReplay({ bufferSize: 1, refCount: true })
)
- this.isAnonymous$ = this.me$.pipe(map((user) => !user || !('id' in user)))
+
+ this.isUserAnonymous$ = this.me$.pipe(
+ map((user) => !user || !('id' in user))
+ )
+
this.users$ = this.usersApi.getUsers().pipe(
map((users) => users.map((user) => this.mapper.userFromApi(user))),
shareReplay()
@@ -91,7 +100,7 @@ export class Gn4PlatformService implements PlatformServiceInterface {
}
isAnonymous(): Observable {
- return this.isAnonymous$
+ return this.isUserAnonymous$
}
getOrganizations(): Observable {
@@ -148,6 +157,24 @@ export class Gn4PlatformService implements PlatformServiceInterface {
),
shareReplay(1)
)
+
return this.thesauri[uri]
}
+
+ getUserFeedbacks(uuid: string): Observable {
+ return this.userfeedbackApiService
+ .getUserComments(uuid)
+ .pipe(
+ map((userFeedbacks) =>
+ userFeedbacks.map(this.mapper.userFeedbacksFromApi)
+ )
+ )
+ }
+
+ postUserFeedbacks(userFeedback: UserFeedback): Observable {
+ const mappedUserFeedBack = this.mapper.userFeedbacksToApi(userFeedback)
+ return this.userfeedbackApiService
+ .newUserFeedback(mappedUserFeedBack)
+ .pipe(map(() => undefined))
+ }
}
diff --git a/libs/common/domain/src/lib/model/record/index.ts b/libs/common/domain/src/lib/model/record/index.ts
index f312e98382..59c495239e 100644
--- a/libs/common/domain/src/lib/model/record/index.ts
+++ b/libs/common/domain/src/lib/model/record/index.ts
@@ -1,3 +1,4 @@
export * from './contact.model'
export * from './organization.model'
export * from './metadata.model'
+export * from './user-feedbacks.model'
diff --git a/libs/common/domain/src/lib/model/record/user-feedbacks.model.ts b/libs/common/domain/src/lib/model/record/user-feedbacks.model.ts
new file mode 100644
index 0000000000..59008ea9ca
--- /dev/null
+++ b/libs/common/domain/src/lib/model/record/user-feedbacks.model.ts
@@ -0,0 +1,15 @@
+export interface UserFeedback {
+ uuid: string
+ comment: string
+ metadataUUID: string
+ authorUserId: string
+ authorName: string
+ authorEmail: string
+ published: boolean
+ parentUuid?: string
+ date: Date
+}
+
+export interface UserFeedbackViewModel extends UserFeedback {
+ avatarUrl: string
+}
diff --git a/libs/common/domain/src/lib/platform.service.interface.ts b/libs/common/domain/src/lib/platform.service.interface.ts
index 1f6acc2745..545735a6f4 100644
--- a/libs/common/domain/src/lib/platform.service.interface.ts
+++ b/libs/common/domain/src/lib/platform.service.interface.ts
@@ -2,6 +2,7 @@ import type { Observable } from 'rxjs'
import type { UserModel } from './model/user/user.model'
import type { Organization } from './model/record/organization.model'
import type { ThesaurusModel } from './model/thesaurus/'
+import { UserFeedback } from './model/record'
export abstract class PlatformServiceInterface {
abstract getType(): string
@@ -16,4 +17,6 @@ export abstract class PlatformServiceInterface {
abstract getOrganizations(): Observable
abstract translateKey(key: string): Observable
abstract getThesaurusByUri(uri: string): Observable
+ abstract getUserFeedbacks(recordUuid: string): Observable
+ abstract postUserFeedbacks(recordUuid: UserFeedback): Observable
}
diff --git a/libs/common/fixtures/src/index.ts b/libs/common/fixtures/src/index.ts
index 293c434ea3..850a472de6 100644
--- a/libs/common/fixtures/src/index.ts
+++ b/libs/common/fixtures/src/index.ts
@@ -1,11 +1,13 @@
+export * from './lib/elasticsearch'
+export * from './lib/gn4'
+export * from './lib/search'
+
export * from './lib/geojson.fixtures'
+export * from './lib/link.fixtures'
export * from './lib/ol-feature.fixture'
+export * from './lib/organisations.fixture'
export * from './lib/record-link.fixtures'
-export * from './lib/link.fixtures'
export * from './lib/records.fixtures'
-export * from './lib/organisations.fixture'
-export * from './lib/elasticsearch'
-export * from './lib/search'
-export * from './lib/user.fixtures'
export * from './lib/repository.fixtures'
-export * from './lib/gn4'
+export * from './lib/user.fixtures'
+export * from './lib/user-feedbacks.fixtures'
diff --git a/libs/common/fixtures/src/lib/records.fixtures.ts b/libs/common/fixtures/src/lib/records.fixtures.ts
index 8def498eab..9555fda043 100644
--- a/libs/common/fixtures/src/lib/records.fixtures.ts
+++ b/libs/common/fixtures/src/lib/records.fixtures.ts
@@ -1,5 +1,5 @@
import { DatasetRecord } from '@geonetwork-ui/common/domain/model/record'
-import { deepFreeze } from './utils/freeze'
+import { deepFreeze } from './utils'
export const DATASET_RECORDS: DatasetRecord[] = deepFreeze([
{
diff --git a/libs/common/fixtures/src/lib/user-feedbacks.fixtures.ts b/libs/common/fixtures/src/lib/user-feedbacks.fixtures.ts
new file mode 100644
index 0000000000..f4bcc11413
--- /dev/null
+++ b/libs/common/fixtures/src/lib/user-feedbacks.fixtures.ts
@@ -0,0 +1,83 @@
+import { UserFeedback } from '@geonetwork-ui/common/domain/model/record'
+import { deepFreeze } from './utils'
+
+export const SOME_USER_FEEDBACKS: UserFeedback[] = deepFreeze([
+ {
+ uuid: '4ad03fb7-1728-424c-bdaa-aedd531b07a8',
+ comment: 'A nice comment.',
+ metadataUUID: 'my-dataset-001',
+ authorUserId: '46798',
+ authorName: 'Arnaud De Maison',
+ authorEmail: 'a.demaison@geo2france.fr',
+ published: true,
+ parentUuid: undefined,
+ date: new Date('2023-01-01T08:00:00Z'),
+ },
+ {
+ uuid: '52cbd0f1-9cb9-4409-8e85-bc608f049af4',
+ comment: 'A very nice comment that is a reply.',
+ metadataUUID: 'my-dataset-001',
+ authorUserId: '46798',
+ authorName: 'Arnaud De Maison',
+ authorEmail: 'a.demaison@geo2france.fr',
+ published: true,
+ parentUuid: '4ad03fb7-1728-424c-bdaa-aedd531b07a8',
+ date: new Date('2023-01-01T09:00:00Z'),
+ },
+ {
+ uuid: 'b48f62ec-b5e6-4d27-a396-2c2b44f6dcb5',
+ comment: 'Another nice comment.',
+ metadataUUID: 'my-dataset-001',
+ authorUserId: '46798',
+ authorName: 'Arnaud De Maison',
+ authorEmail: 'a.demaison@geo2france.fr',
+ published: true,
+ parentUuid: undefined,
+ date: new Date('2023-01-01T10:00:00Z'),
+ },
+ {
+ uuid: '1f12a3be-fc8a-4e83-968f-9b88ffbcab02',
+ comment: 'Another very nice reply.',
+ metadataUUID: 'my-dataset-001',
+ authorUserId: '46798',
+ authorName: 'Arnaud De Maison',
+ authorEmail: 'a.demaison@geo2france.fr',
+ published: true,
+ parentUuid: '4ad03fb7-1728-424c-bdaa-aedd531b07a8',
+ date: new Date('2023-01-01T11:00:00Z'),
+ },
+ {
+ uuid: 'f8dd778d-e93c-4b3c-ba9b-9574be070f46',
+ comment: 'Another nice comment.',
+ metadataUUID: 'my-dataset-001',
+ authorUserId: '46798',
+ authorName: 'Arnaud De Maison',
+ authorEmail: 'a.demaison@geo2france.fr',
+ published: true,
+ parentUuid: undefined,
+ date: new Date('2023-01-01T10:00:00Z'),
+ },
+ {
+ uuid: 'df3b8872-61d1-4ae9-8822-bb070b94d7d1',
+ comment: 'Another nice comment.',
+ metadataUUID: 'my-dataset-001',
+ authorUserId: '46798',
+ authorName: 'Arnaud De Maison',
+ authorEmail: 'a.demaison@geo2france.fr',
+ published: true,
+ parentUuid: undefined,
+ date: new Date('2023-01-01T11:00:00Z'),
+ },
+])
+
+export const A_USER_FEEDBACK: UserFeedback = deepFreeze({
+ uuid: '4ad03fb7-1728-424c-bdaa-aedd531b07a8',
+ comment: 'A nice comment.',
+ metadataUUID: 'my-dataset-001',
+ authorUserId: '46798',
+ authorName: 'Arnaud De Maison',
+ authorEmail: 'a.demaison@geo2france.fr',
+ published: true,
+ parentUuid: undefined,
+ date: new Date('2023-01-01T08:00:00Z'),
+})
diff --git a/libs/feature/record/src/lib/feature-record.module.ts b/libs/feature/record/src/lib/feature-record.module.ts
index 372a422605..3c9eee16e4 100644
--- a/libs/feature/record/src/lib/feature-record.module.ts
+++ b/libs/feature/record/src/lib/feature-record.module.ts
@@ -11,7 +11,10 @@ import { MdViewFacade } from './state'
import { MdViewEffects } from './state/mdview.effects'
import { MapViewComponent } from './map-view/map-view.component'
import { DataViewComponent } from './data-view/data-view.component'
-import { MD_VIEW_FEATURE_STATE_KEY, reducer } from './state/mdview.reducer'
+import {
+ METADATA_VIEW_FEATURE_STATE_KEY,
+ reducer,
+} from './state/mdview.reducer'
import { MatTabsModule } from '@angular/material/tabs'
import { MatIconModule } from '@angular/material/icon'
import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets'
@@ -35,7 +38,7 @@ import { DataViewShareComponent } from './data-view-share/data-view-share.compon
],
imports: [
CommonModule,
- StoreModule.forFeature(MD_VIEW_FEATURE_STATE_KEY, reducer),
+ StoreModule.forFeature(METADATA_VIEW_FEATURE_STATE_KEY, reducer),
EffectsModule.forFeature([MdViewEffects]),
UiLayoutModule,
FeatureMapModule,
diff --git a/libs/feature/record/src/lib/state/mdview.actions.ts b/libs/feature/record/src/lib/state/mdview.actions.ts
index 9b2172888c..9cc84be989 100644
--- a/libs/feature/record/src/lib/state/mdview.actions.ts
+++ b/libs/feature/record/src/lib/state/mdview.actions.ts
@@ -1,7 +1,13 @@
import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model'
import { createAction, props } from '@ngrx/store'
-import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
+import {
+ CatalogRecord,
+ UserFeedback,
+} from '@geonetwork-ui/common/domain/model/record'
+/*
+ Metadata actions
+ */
export const loadFullMetadata = createAction(
'[Metadata view] Load full metadata',
props<{ uuid: string }>()
@@ -12,24 +18,63 @@ export const setIncompleteMetadata = createAction(
props<{ incomplete: Partial }>()
)
-export const loadFullSuccess = createAction(
- '[Metadata view] Load full success',
+export const loadFullMetadataSuccess = createAction(
+ '[Metadata view] Load full metadata success',
props<{ full: CatalogRecord }>()
)
-export const loadFullFailure = createAction(
- '[Metadata view] Load full failure',
+export const loadFullMetadataFailure = createAction(
+ '[Metadata view] Load full metadata failure',
props<{ otherError?: string; notFound?: boolean }>()
)
+export const closeMetadata = createAction('[Metadata view] close')
+
+/*
+ Related actions
+ */
export const setRelated = createAction(
'[Metadata view] Set related records',
props<{ related: CatalogRecord[] }>()
)
+/*
+ ChartConfig actions
+ */
export const setChartConfig = createAction(
'[Metadata view] Set chart config',
props<{ chartConfig: DatavizConfigurationModel }>()
)
-export const close = createAction('[Metadata view] close')
+/*
+ User Feedbacks actions
+ */
+export const addUserFeedback = createAction(
+ '[Metadata view] Add UserFeedback',
+ props<{ userFeedback: UserFeedback }>()
+)
+
+export const addUserFeedbackSuccess = createAction(
+ '[Metadata view] Add UserFeedback Success',
+ props<{ datasetUuid: string }>()
+)
+
+export const addUserFeedbackFailure = createAction(
+ '[Metadata view] Add UserFeedback Failure',
+ props<{ otherError?: string; notFound?: boolean }>()
+)
+
+export const loadUserFeedbacks = createAction(
+ '[Metadata view] Load UserFeedbacks',
+ props<{ datasetUuid: string }>()
+)
+
+export const loadUserFeedbacksSuccess = createAction(
+ '[Metadata view] Load UserFeedbacks Success',
+ props<{ userFeedbacks: UserFeedback[] }>()
+)
+
+export const loadUserFeedbacksFailure = createAction(
+ '[Metadata view] Load UserFeedbacks Failure',
+ props<{ otherError?: string; notFound?: boolean }>()
+)
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 23b11a8804..066c5abe60 100644
--- a/libs/feature/record/src/lib/state/mdview.effects.spec.ts
+++ b/libs/feature/record/src/lib/state/mdview.effects.spec.ts
@@ -1,8 +1,10 @@
import { TestBed } from '@angular/core/testing'
import {
+ A_USER_FEEDBACK,
DATASET_RECORDS,
SAMPLE_AGGREGATIONS_RESULTS,
SAMPLE_SEARCH_RESULTS,
+ SOME_USER_FEEDBACKS,
} from '@geonetwork-ui/common/fixtures'
import { provideMockActions } from '@ngrx/effects/testing'
@@ -15,6 +17,7 @@ import { MdViewEffects } from './mdview.effects'
import { hot } from 'jasmine-marbles'
import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
import { RecordsRepositoryInterface } from '@geonetwork-ui/common/domain/repository/records-repository.interface'
+import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface'
const full = {
uniqueIdentifier: '1231321321',
@@ -29,10 +32,16 @@ class RecordsRepositoryMock {
getSimilarRecords = jest.fn(() => of(DATASET_RECORDS))
}
+class PlatformServiceInterfaceMock {
+ getUserFeedbacks = jest.fn(() => of(SOME_USER_FEEDBACKS))
+ postUserFeedbacks = jest.fn(() => of(undefined))
+}
+
describe('MdViewEffects', () => {
let actions: Observable
let effects: MdViewEffects
let repository: RecordsRepositoryInterface
+ let platform: PlatformServiceInterface
beforeEach(() => {
TestBed.configureTestingModule({
@@ -45,23 +54,30 @@ describe('MdViewEffects', () => {
provide: RecordsRepositoryInterface,
useClass: RecordsRepositoryMock,
},
+ {
+ provide: PlatformServiceInterface,
+ useClass: PlatformServiceInterfaceMock,
+ },
],
})
repository = TestBed.inject(RecordsRepositoryInterface)
effects = TestBed.inject(MdViewEffects)
+ platform = TestBed.inject(PlatformServiceInterface)
})
- describe('loadFullRecord$', () => {
+ describe('loadFullMetadata$', () => {
describe('when api success and at least one record found', () => {
it('dispatch loadFullSuccess', () => {
actions = hot('-a-|', {
a: MdViewActions.loadFullMetadata({ uuid: full.uniqueIdentifier }),
})
const expected = hot('-a-|', {
- a: MdViewActions.loadFullSuccess({ full: DATASET_RECORDS[0] }),
+ a: MdViewActions.loadFullMetadataSuccess({
+ full: DATASET_RECORDS[0],
+ }),
})
- expect(effects.loadFull$).toBeObservable(expected)
+ expect(effects.loadFullMetadata$).toBeObservable(expected)
})
})
describe('when api success and at no record found', () => {
@@ -73,9 +89,9 @@ describe('MdViewEffects', () => {
a: MdViewActions.loadFullMetadata({ uuid: full.uniqueIdentifier }),
})
const expected = hot('-a-|', {
- a: MdViewActions.loadFullFailure({ notFound: true }),
+ a: MdViewActions.loadFullMetadataFailure({ notFound: true }),
})
- expect(effects.loadFull$).toBeObservable(expected)
+ expect(effects.loadFullMetadata$).toBeObservable(expected)
})
})
@@ -90,9 +106,9 @@ describe('MdViewEffects', () => {
a: MdViewActions.loadFullMetadata({ uuid: full.uniqueIdentifier }),
})
const expected = hot('-(a|)', {
- a: MdViewActions.loadFullFailure({ otherError: 'api' }),
+ a: MdViewActions.loadFullMetadataFailure({ otherError: 'api' }),
})
- expect(effects.loadFull$).toBeObservable(expected)
+ expect(effects.loadFullMetadata$).toBeObservable(expected)
})
})
})
@@ -101,7 +117,7 @@ describe('MdViewEffects', () => {
describe('when load full success', () => {
it('dispatch setRelated', () => {
actions = hot('-a-|', {
- a: MdViewActions.loadFullSuccess({ full }),
+ a: MdViewActions.loadFullMetadataSuccess({ full }),
})
const expected = hot('-a-|', {
a: MdViewActions.setRelated({ related: DATASET_RECORDS }),
@@ -115,7 +131,7 @@ describe('MdViewEffects', () => {
})
it('dispatch loadFullFailure', () => {
actions = hot('-a-|', {
- a: MdViewActions.loadFullSuccess({ full }),
+ a: MdViewActions.loadFullMetadataSuccess({ full }),
})
const expected = hot('-(a|)', {
a: MdViewActions.setRelated({ related: null }),
@@ -124,4 +140,122 @@ describe('MdViewEffects', () => {
})
})
})
+
+ describe('loadUserFeedbacks$', () => {
+ describe('when loadUserFeedbacks success', () => {
+ it('should dispatch loadUserFeedbacksSuccess when API call is successful', () => {
+ actions = hot('-a-', {
+ a: MdViewActions.loadUserFeedbacks({ datasetUuid: '12345' }),
+ })
+ const expected = hot('-a-', {
+ a: MdViewActions.loadUserFeedbacksSuccess({
+ userFeedbacks: SOME_USER_FEEDBACKS,
+ }),
+ })
+
+ expect(effects.loadUserFeedbacks$).toBeObservable(expected)
+ })
+ })
+
+ describe('when api fails', () => {
+ const error = 'API error'
+
+ beforeEach(() => {
+ platform.getUserFeedbacks = jest.fn(() =>
+ throwError(() => new Error(error))
+ )
+ })
+
+ it('should dispatch loadUserFeedbacksFailure when API call fails', () => {
+ actions = hot('-a|', {
+ a: MdViewActions.loadUserFeedbacks({ datasetUuid: '12345' }),
+ })
+ const expected = hot('-a|', {
+ a: MdViewActions.loadUserFeedbacksFailure({ otherError: error }),
+ })
+
+ expect(effects.loadUserFeedbacks$).toBeObservable(expected)
+ })
+ })
+ })
+
+ describe('reloadUserFeedbacks$', () => {
+ describe('when addUserFeedbackSuccess', () => {
+ it('should dispatch loadUserFeedbacksSuccess when API call is successful', () => {
+ actions = hot('-a-', {
+ a: MdViewActions.addUserFeedbackSuccess({ datasetUuid: '12345' }),
+ })
+ const expected = hot('-a', {
+ a: MdViewActions.loadUserFeedbacksSuccess({
+ userFeedbacks: SOME_USER_FEEDBACKS,
+ }),
+ })
+
+ expect(effects.reloadUserFeedbacks$).toBeObservable(expected)
+ })
+ })
+
+ describe('when api fails', () => {
+ const error = 'API error'
+
+ beforeEach(() => {
+ platform.getUserFeedbacks = jest.fn(() =>
+ throwError(() => new Error(error))
+ )
+ })
+
+ it('should dispatch loadUserFeedbacksFailure when API call fails', () => {
+ const error = 'API error'
+
+ actions = hot('-a-', {
+ a: MdViewActions.addUserFeedbackSuccess({ datasetUuid: '12345' }),
+ })
+ const expected = hot('-a', {
+ a: MdViewActions.loadUserFeedbacksFailure({ otherError: error }),
+ })
+
+ expect(effects.reloadUserFeedbacks$).toBeObservable(expected)
+ })
+ })
+ })
+
+ describe('addUserFeedback$', () => {
+ describe('when addUserFeedback success', () => {
+ it('should dispatch addUserFeedbackSuccess when API call is successful', () => {
+ actions = hot('-a-', {
+ a: MdViewActions.addUserFeedback({ userFeedback: A_USER_FEEDBACK }),
+ })
+ const expected = hot('-a-', {
+ a: MdViewActions.addUserFeedbackSuccess({
+ datasetUuid: A_USER_FEEDBACK.metadataUUID,
+ }),
+ })
+
+ expect(effects.addUserFeedback$).toBeObservable(expected)
+ })
+ })
+
+ describe('when api fails', () => {
+ const error = 'API error'
+
+ beforeEach(() => {
+ platform.postUserFeedbacks = jest.fn(() =>
+ throwError(() => new Error(error))
+ )
+ })
+
+ it('should dispatch addUserFeedbackFailure when API call fails', () => {
+ const error = 'API error'
+
+ actions = hot('-a-', {
+ a: MdViewActions.addUserFeedback({ userFeedback: A_USER_FEEDBACK }),
+ })
+ const expected = hot('-a', {
+ a: MdViewActions.addUserFeedbackFailure({ otherError: error }),
+ })
+
+ expect(effects.addUserFeedback$).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 66893f90f4..ac98891858 100644
--- a/libs/feature/record/src/lib/state/mdview.effects.ts
+++ b/libs/feature/record/src/lib/state/mdview.effects.ts
@@ -1,18 +1,23 @@
import { Injectable } from '@angular/core'
import { Actions, createEffect, ofType } from '@ngrx/effects'
-import { of } from 'rxjs'
+import { exhaustMap, mergeMap, of } from 'rxjs'
import { catchError, map, switchMap } 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(
private actions$: Actions,
- private recordsRepository: RecordsRepositoryInterface
+ private recordsRepository: RecordsRepositoryInterface,
+ private platformServiceInterface: PlatformServiceInterface
) {}
- loadFull$ = createEffect(() =>
+ /*
+ Metadata effects
+ */
+ loadFullMetadata$ = createEffect(() =>
this.actions$.pipe(
ofType(MdViewActions.loadFullMetadata),
switchMap(({ uuid }) =>
@@ -20,19 +25,22 @@ export class MdViewEffects {
),
map((record) => {
if (record === null) {
- return MdViewActions.loadFullFailure({ notFound: true })
+ return MdViewActions.loadFullMetadataFailure({ notFound: true })
}
- return MdViewActions.loadFullSuccess({ full: record })
+ return MdViewActions.loadFullMetadataSuccess({ full: record })
}),
catchError((error) =>
- of(MdViewActions.loadFullFailure({ otherError: error.message }))
+ of(MdViewActions.loadFullMetadataFailure({ otherError: error.message }))
)
)
)
+ /*
+ Related effects
+ */
loadRelatedRecords$ = createEffect(() =>
this.actions$.pipe(
- ofType(MdViewActions.loadFullSuccess),
+ ofType(MdViewActions.loadFullMetadataSuccess),
switchMap(({ full }) => this.recordsRepository.getSimilarRecords(full)),
map((related) => {
return MdViewActions.setRelated({ related })
@@ -40,4 +48,71 @@ export class MdViewEffects {
catchError((error) => of(MdViewActions.setRelated({ related: null })))
)
)
+
+ /*
+ UserFeedback effects
+ */
+ loadUserFeedbacks$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(MdViewActions.loadUserFeedbacks),
+ exhaustMap(({ datasetUuid }) =>
+ this.platformServiceInterface.getUserFeedbacks(datasetUuid).pipe(
+ map((userFeedbacks) =>
+ MdViewActions.loadUserFeedbacksSuccess({ userFeedbacks })
+ ),
+ catchError((error) =>
+ of(
+ MdViewActions.loadUserFeedbacksFailure({
+ otherError: error.message,
+ })
+ )
+ )
+ )
+ )
+ )
+ )
+
+ reloadUserFeedbacks$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(MdViewActions.addUserFeedbackSuccess),
+ exhaustMap(({ datasetUuid }) =>
+ this.platformServiceInterface.getUserFeedbacks(datasetUuid).pipe(
+ map((userFeedbacks) =>
+ MdViewActions.loadUserFeedbacksSuccess({ userFeedbacks })
+ ),
+ catchError((error) =>
+ of(
+ MdViewActions.loadUserFeedbacksFailure({
+ otherError: error.message,
+ })
+ )
+ )
+ )
+ )
+ )
+ )
+
+ addUserFeedback$ = createEffect(() =>
+ this.actions$.pipe(
+ ofType(MdViewActions.addUserFeedback),
+ mergeMap((action) =>
+ this.platformServiceInterface
+ .postUserFeedbacks(action.userFeedback)
+ .pipe(
+ map(() =>
+ MdViewActions.addUserFeedbackSuccess({
+ datasetUuid: action.userFeedback.metadataUUID,
+ })
+ ),
+ catchError((error) => {
+ return of(
+ MdViewActions.addUserFeedbackFailure({
+ otherError: error.message,
+ })
+ )
+ })
+ )
+ )
+ )
+ )
}
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 f915487c2f..e83b9dbdd4 100644
--- a/libs/feature/record/src/lib/state/mdview.facade.spec.ts
+++ b/libs/feature/record/src/lib/state/mdview.facade.spec.ts
@@ -1,23 +1,39 @@
import { TestBed } from '@angular/core/testing'
import { MockStore, provideMockStore } from '@ngrx/store/testing'
-import { initialMdviewState, MD_VIEW_FEATURE_STATE_KEY } from './mdview.reducer'
+import {
+ initialMetadataViewState,
+ METADATA_VIEW_FEATURE_STATE_KEY,
+} from './mdview.reducer'
import { MdViewFacade } from './mdview.facade'
import * as MdViewActions from './mdview.actions'
import { hot } from 'jasmine-marbles'
-import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures'
+import {
+ A_USER_FEEDBACK,
+ DATASET_RECORDS,
+} from '@geonetwork-ui/common/fixtures'
+import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model'
+import { AvatarServiceInterface } from '@geonetwork-ui/api/repository'
describe('MdViewFacade', () => {
let store: MockStore
let facade: MdViewFacade
+ const chartConfigMock: DatavizConfigurationModel = {
+ aggregation: 'sum',
+ xProperty: 'anneeappro',
+ yProperty: 'nbre_com',
+ chartType: 'bar',
+ }
+
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
MdViewFacade,
+ AvatarServiceInterface,
provideMockStore({
initialState: {
- [MD_VIEW_FEATURE_STATE_KEY]: initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: initialMetadataViewState,
},
}),
],
@@ -25,15 +41,17 @@ describe('MdViewFacade', () => {
store = TestBed.inject(MockStore)
facade = TestBed.inject(MdViewFacade)
})
+
describe('isPresent$', () => {
it('emits false if no metadata', () => {
const expected = hot('a', { a: false })
expect(facade.isPresent$).toBeObservable(expected)
})
+
it('emits true if metadata', () => {
store.setState({
- [MD_VIEW_FEATURE_STATE_KEY]: {
- ...initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: {
+ ...initialMetadataViewState,
metadata: DATASET_RECORDS[0],
},
})
@@ -41,15 +59,17 @@ describe('MdViewFacade', () => {
expect(facade.isPresent$).toBeObservable(expected)
})
})
+
describe('metadata$', () => {
it('does not emit if no metadata', () => {
const expected = hot('-')
expect(facade.metadata$).toBeObservable(expected)
})
+
it('emits metadata if present', () => {
store.setState({
- [MD_VIEW_FEATURE_STATE_KEY]: {
- ...initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: {
+ ...initialMetadataViewState,
metadata: DATASET_RECORDS[0],
},
})
@@ -57,15 +77,17 @@ describe('MdViewFacade', () => {
expect(facade.metadata$).toBeObservable(expected)
})
})
+
describe('allLinks$', () => {
it('does not emit if no links', () => {
const expected = hot('-')
expect(facade.allLinks$).toBeObservable(expected)
})
+
it('emits allLinks if present', () => {
store.setState({
- [MD_VIEW_FEATURE_STATE_KEY]: {
- ...initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: {
+ ...initialMetadataViewState,
metadata: DATASET_RECORDS[0],
},
})
@@ -73,11 +95,12 @@ describe('MdViewFacade', () => {
expect(facade.allLinks$).toBeObservable(expected)
})
})
+
describe('isIncomplete$', () => {
it('emits true if full record is loading', () => {
store.setState({
- [MD_VIEW_FEATURE_STATE_KEY]: {
- ...initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: {
+ ...initialMetadataViewState,
metadata: DATASET_RECORDS[0],
loadingFull: true,
},
@@ -85,10 +108,11 @@ describe('MdViewFacade', () => {
const expected = hot('a', { a: true })
expect(facade.isIncomplete$).toBeObservable(expected)
})
+
it('emits false if full metadata loaded', () => {
store.setState({
- [MD_VIEW_FEATURE_STATE_KEY]: {
- ...initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: {
+ ...initialMetadataViewState,
metadata: DATASET_RECORDS[0],
loadingFull: false,
},
@@ -96,45 +120,52 @@ describe('MdViewFacade', () => {
const expected = hot('a', { a: false })
expect(facade.isIncomplete$).toBeObservable(expected)
})
+
it('does not emit if no metadata', () => {
const expected = hot('-')
expect(facade.isIncomplete$).toBeObservable(expected)
})
})
+
describe('error$', () => {
let values
+
beforeEach(() => {
values = []
facade.error$.subscribe((v) => values.push(v))
})
+
it('emits the error if any', () => {
store.setState({
- [MD_VIEW_FEATURE_STATE_KEY]: {
- ...initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: {
+ ...initialMetadataViewState,
error: 'something went wrong',
},
})
expect(values).toEqual([null, 'something went wrong'])
})
+
it('emits null if no error', () => {
expect(values).toEqual([null])
})
+
it('emits the error and null', () => {
store.setState({
- [MD_VIEW_FEATURE_STATE_KEY]: {
- ...initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: {
+ ...initialMetadataViewState,
error: 'something went wrong',
},
})
store.setState({
- [MD_VIEW_FEATURE_STATE_KEY]: {
- ...initialMdviewState,
+ [METADATA_VIEW_FEATURE_STATE_KEY]: {
+ ...initialMetadataViewState,
error: null,
},
})
expect(values).toEqual([null, 'something went wrong', null])
})
})
+
describe('setIncompleteMetadata', () => {
it('dispatches a setIncompleteMetadata action', () => {
facade.setIncompleteMetadata(DATASET_RECORDS[0])
@@ -146,20 +177,42 @@ describe('MdViewFacade', () => {
expect(store.scannedActions$).toBeObservable(expected)
})
})
- describe('close', () => {
+
+ describe('closeMetadata', () => {
it('dispatches a close action', () => {
- facade.close()
+ facade.closeMetadata()
const expected = hot('a', {
- a: MdViewActions.close(),
+ a: MdViewActions.closeMetadata(),
})
expect(store.scannedActions$).toBeObservable(expected)
})
})
+
describe('setChartConfig', () => {
it('dispatches a setChartConfig action', () => {
- facade.setChartConfig()
+ facade.setChartConfig(chartConfigMock)
+ const expected = hot('a', {
+ a: MdViewActions.setChartConfig({ chartConfig: chartConfigMock }),
+ })
+ expect(store.scannedActions$).toBeObservable(expected)
+ })
+ })
+
+ describe('addUserFeedback', () => {
+ it('dispatches a addUserFeedback action', () => {
+ facade.addUserFeedback(A_USER_FEEDBACK)
+ const expected = hot('a', {
+ a: MdViewActions.addUserFeedback({ userFeedback: A_USER_FEEDBACK }),
+ })
+ expect(store.scannedActions$).toBeObservable(expected)
+ })
+ })
+
+ describe('loadUserFeedbacks', () => {
+ it('dispatches a loadUserFeedbacks action', () => {
+ facade.loadUserFeedbacks(expect.any(Number))
const expected = hot('a', {
- a: MdViewActions.setChartConfig(),
+ a: MdViewActions.loadUserFeedbacks({ datasetUuid: expect.any(Number) }),
})
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 a2e4a0064f..c8aa010cbb 100644
--- a/libs/feature/record/src/lib/state/mdview.facade.ts
+++ b/libs/feature/record/src/lib/state/mdview.facade.ts
@@ -5,7 +5,12 @@ import * as MdViewActions from './mdview.actions'
import * as MdViewSelectors from './mdview.selectors'
import { LinkClassifierService, LinkUsage } from '@geonetwork-ui/util/shared'
import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model'
-import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
+import {
+ CatalogRecord,
+ UserFeedback,
+ UserFeedbackViewModel,
+} from '@geonetwork-ui/common/domain/model/record'
+import { AvatarServiceInterface } from '@geonetwork-ui/api/repository'
@Injectable()
/**
@@ -15,19 +20,31 @@ import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
* To clear the current record use the `close()` method.
*/
export class MdViewFacade {
+ constructor(
+ private store: Store,
+ private linkClassifier: LinkClassifierService,
+ private avatarService: AvatarServiceInterface
+ ) {}
+
isPresent$ = this.store.pipe(
select(MdViewSelectors.getMetadataUuid),
map((uuid) => !!uuid)
)
- isLoading$ = this.store.pipe(select(MdViewSelectors.getMetadataIsLoading))
+
+ isMetadataLoading$ = this.store.pipe(
+ select(MdViewSelectors.getMetadataIsLoading)
+ )
+
metadata$ = this.store.pipe(
select(MdViewSelectors.getMetadata),
filter((md) => !!md)
)
+
isIncomplete$ = this.store.pipe(
select(MdViewSelectors.getMetadataIsIncomplete),
filter((incomplete) => incomplete !== null)
)
+
error$ = this.store.pipe(select(MdViewSelectors.getMetadataError))
related$ = this.store.pipe(select(MdViewSelectors.getRelated))
@@ -37,11 +54,13 @@ export class MdViewFacade {
allLinks$ = this.metadata$.pipe(
map((record) => ('distributions' in record ? record.distributions : []))
)
+
apiLinks$ = this.allLinks$.pipe(
map((links) =>
links.filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.API))
)
)
+
mapApiLinks$ = this.allLinks$.pipe(
map((links) =>
links.filter((link) =>
@@ -49,6 +68,7 @@ export class MdViewFacade {
)
)
)
+
downloadLinks$ = this.allLinks$.pipe(
map((links) =>
links.filter((link) =>
@@ -56,11 +76,13 @@ export class MdViewFacade {
)
)
)
+
dataLinks$ = this.allLinks$.pipe(
map((links) =>
links.filter((link) => this.linkClassifier.hasUsage(link, LinkUsage.DATA))
)
)
+
geoDataLinks$ = this.allLinks$.pipe(
map((links) =>
links.filter((link) =>
@@ -68,9 +90,11 @@ export class MdViewFacade {
)
)
)
+
landingPageLinks$ = this.metadata$.pipe(
map((record) => ('landingPage' in record ? [record.landingPage] : []))
)
+
otherLinks$ = this.allLinks$.pipe(
map((links) =>
links.filter((link) =>
@@ -79,10 +103,13 @@ export class MdViewFacade {
)
)
- constructor(
- private store: Store,
- private linkClassifier: LinkClassifierService
- ) {}
+ userFeedbacks$ = this.store.pipe(select(MdViewSelectors.getUserFeedbacks))
+ isAllUserFeedbackLoading$ = this.store.pipe(
+ select(MdViewSelectors.getAllUserFeedbacksLoading)
+ )
+ isAddUserFeedbackLoading$ = this.store.pipe(
+ select(MdViewSelectors.getAddUserFeedbacksLoading)
+ )
/**
* This will show an incomplete record (e.g. from a search result) as a preview
@@ -91,16 +118,30 @@ export class MdViewFacade {
setIncompleteMetadata(incomplete: CatalogRecord) {
this.store.dispatch(MdViewActions.setIncompleteMetadata({ incomplete }))
}
+
/**
* This will trigger the load of a full metadata record
*/
loadFull(uuid: string) {
this.store.dispatch(MdViewActions.loadFullMetadata({ uuid }))
}
- close() {
- this.store.dispatch(MdViewActions.close())
+
+ closeMetadata() {
+ this.store.dispatch(MdViewActions.closeMetadata())
}
+
setChartConfig(chartConfig: DatavizConfigurationModel) {
this.store.dispatch(MdViewActions.setChartConfig({ chartConfig }))
}
+
+ /**
+ * UserFeedbacks
+ */
+ addUserFeedback(userFeedback: UserFeedback) {
+ this.store.dispatch(MdViewActions.addUserFeedback({ userFeedback }))
+ }
+
+ loadUserFeedbacks(datasetUuid: string) {
+ this.store.dispatch(MdViewActions.loadUserFeedbacks({ datasetUuid }))
+ }
}
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 148f42bc96..3bc1acfca9 100644
--- a/libs/feature/record/src/lib/state/mdview.reducer.spec.ts
+++ b/libs/feature/record/src/lib/state/mdview.reducer.spec.ts
@@ -1,47 +1,51 @@
import * as MdViewActions from './mdview.actions'
-import { initialMdviewState, reducer } from './mdview.reducer'
-import { DATASET_RECORDS } from '@geonetwork-ui/common/fixtures'
+import { initialMetadataViewState, reducer } from './mdview.reducer'
+import {
+ A_USER_FEEDBACK,
+ DATASET_RECORDS,
+ SOME_USER_FEEDBACKS,
+} from '@geonetwork-ui/common/fixtures'
+import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model'
-const chartConfigMock = {
+const chartConfigMock: DatavizConfigurationModel = {
aggregation: 'sum',
xProperty: 'anneeappro',
yProperty: 'nbre_com',
chartType: 'bar',
}
-const withErrorMdViewState = {
- ...initialMdviewState,
- error: { otherError: 'Some error' },
-}
-
-describe('MdView Reducer', () => {
+describe('metadataViewReducer', () => {
describe('undefined action', () => {
it('should return the default state', () => {
const action = {} as any
const state = reducer(undefined, action)
- expect(state).toBe(initialMdviewState)
+ expect(state).toBe(initialMetadataViewState)
})
})
describe('loadFullMetadata', () => {
let action
+
beforeEach(() => {
action = MdViewActions.loadFullMetadata({
- uniqueIdentifier: '123132132132132132',
+ uuid: '123132132132132132',
})
})
+
it('store the loading state', () => {
- const state = reducer(withErrorMdViewState, action)
+ const state = reducer(initialMetadataViewState, action)
expect(state).toEqual({
- ...withErrorMdViewState,
+ ...initialMetadataViewState,
error: null,
loadingFull: true,
})
})
})
+
describe('setIncompleteMetadata', () => {
let action
+
beforeEach(() => {
const { uniqueIdentifier, title, ...rest } = DATASET_RECORDS[0]
action = MdViewActions.setIncompleteMetadata({
@@ -51,10 +55,11 @@ describe('MdView Reducer', () => {
},
})
})
+
it('saves incomplete metadata', () => {
- const state = reducer(withErrorMdViewState, action)
+ const state = reducer(initialMetadataViewState, action)
expect(state).toEqual({
- ...withErrorMdViewState,
+ ...initialMetadataViewState,
error: null,
metadata: {
title:
@@ -64,92 +69,190 @@ describe('MdView Reducer', () => {
})
})
})
- describe('loadFullRecordSuccess', () => {
+
+ describe('loadFullMetadataSuccess', () => {
let action
+
beforeEach(() => {
- action = MdViewActions.loadFullSuccess({
+ action = MdViewActions.loadFullMetadataSuccess({
full: DATASET_RECORDS[0],
})
})
+
it('saves full metadata ', () => {
const state = reducer(
- { ...withErrorMdViewState, loadingFull: true },
+ { ...initialMetadataViewState, loadingFull: true },
action
)
expect(state).toEqual({
- ...withErrorMdViewState,
+ ...initialMetadataViewState,
error: null,
loadingFull: false,
metadata: DATASET_RECORDS[0],
})
})
})
+
describe('loadFullRecordFailure', () => {
let action
+
beforeEach(() => {
- action = MdViewActions.loadFullFailure({
- otherError: 'error',
+ action = MdViewActions.loadFullMetadataFailure({
+ otherError: 'Some error',
notFound: true,
})
})
+
it('set error', () => {
const state = reducer(
- { ...initialMdviewState, loadingFull: true },
+ { ...initialMetadataViewState, loadingFull: true },
action
)
expect(state).toEqual({
- ...initialMdviewState,
+ ...initialMetadataViewState,
loadingFull: false,
- error: { otherError: 'error', notFound: true },
+ error: { otherError: 'Some error', notFound: true },
})
})
})
+
describe('setRelated', () => {
let action
+
beforeEach(() => {
action = MdViewActions.setRelated({
related: [DATASET_RECORDS[1]],
})
})
+
it('set related records', () => {
- const state = reducer({ ...initialMdviewState }, action)
+ const state = reducer({ ...initialMetadataViewState }, action)
expect(state).toEqual({
- ...initialMdviewState,
+ ...initialMetadataViewState,
related: [DATASET_RECORDS[1]],
})
})
})
+
describe('setChartConfig', () => {
let action
+
beforeEach(() => {
action = MdViewActions.setChartConfig({
- chartConfig: [chartConfigMock],
+ chartConfig: chartConfigMock,
})
})
+
it('set chart config', () => {
- const state = reducer({ ...initialMdviewState }, action)
+ const state = reducer({ ...initialMetadataViewState }, action)
expect(state).toEqual({
- ...initialMdviewState,
- chartConfig: [chartConfigMock],
+ ...initialMetadataViewState,
+ chartConfig: chartConfigMock,
})
})
})
- describe('close', () => {
+
+ describe('closeMetadata', () => {
let action
+
beforeEach(() => {
- action = MdViewActions.close()
+ action = MdViewActions.closeMetadata()
})
+
it('set error', () => {
const state = reducer(
{
- ...initialMdviewState,
+ ...initialMetadataViewState,
related: [DATASET_RECORDS[1]],
loadingFull: false,
metadata: DATASET_RECORDS[0],
},
action
)
- expect(state).toEqual(initialMdviewState)
+ expect(state).toEqual(initialMetadataViewState)
+ })
+ })
+
+ describe('loadUserFeedbacks', () => {
+ let action
+
+ beforeEach(() => {
+ action = MdViewActions.loadUserFeedbacks({
+ datasetUuid: expect.any(Number),
+ })
+ })
+
+ it('return states without error and with allUserFeedbacksLoading true', () => {
+ const state = reducer(initialMetadataViewState, action)
+ expect(state).toEqual({
+ ...initialMetadataViewState,
+ error: null,
+ loadingFull: false,
+ allUserFeedbacksLoading: true,
+ addUserFeedbackLoading: false,
+ })
+ })
+ })
+
+ describe('addUserFeedback', () => {
+ let action
+
+ beforeEach(() => {
+ action = MdViewActions.addUserFeedback({
+ userFeedback: A_USER_FEEDBACK,
+ })
+ })
+
+ it('return states without error and with addUserFeedbackLoading true', () => {
+ const state = reducer({ ...initialMetadataViewState }, action)
+ expect(state).toEqual({
+ ...initialMetadataViewState,
+ addUserFeedbackLoading: true,
+ })
+ })
+ })
+
+ describe('loadUserFeedbacksSuccess', () => {
+ let action
+
+ beforeEach(() => {
+ action = MdViewActions.loadUserFeedbacksSuccess({
+ userFeedbacks: SOME_USER_FEEDBACKS,
+ })
+ })
+
+ it('return states without error and with userfeedbacks', () => {
+ const state = reducer(
+ { ...initialMetadataViewState, allUserFeedbacksLoading: true },
+ action
+ )
+ expect(state).toEqual({
+ ...initialMetadataViewState,
+ error: null,
+ addUserFeedbackLoading: false,
+ allUserFeedbacksLoading: false,
+ loadingFull: false,
+ userFeedbacks: SOME_USER_FEEDBACKS,
+ })
+ })
+ })
+
+ describe('loadUserFeedbacksFailure', () => {
+ let action
+
+ beforeEach(() => {
+ action = MdViewActions.loadUserFeedbacksFailure({
+ otherError: 'Some error',
+ notFound: true,
+ })
+ })
+
+ it('set error', () => {
+ const state = reducer({ ...initialMetadataViewState }, action)
+ expect(state).toEqual({
+ ...initialMetadataViewState,
+ error: { otherError: 'Some error', notFound: true },
+ })
})
})
})
diff --git a/libs/feature/record/src/lib/state/mdview.reducer.ts b/libs/feature/record/src/lib/state/mdview.reducer.ts
index 81b6ba7e08..7e9be2eccd 100644
--- a/libs/feature/record/src/lib/state/mdview.reducer.ts
+++ b/libs/feature/record/src/lib/state/mdview.reducer.ts
@@ -1,61 +1,118 @@
import { Action, createReducer, on } from '@ngrx/store'
-import * as MdViewActions from './mdview.actions'
+import * as MetadataViewActions from './mdview.actions'
import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model'
-import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record'
+import {
+ CatalogRecord,
+ UserFeedback,
+} from '@geonetwork-ui/common/domain/model/record'
-export const MD_VIEW_FEATURE_STATE_KEY = 'mdView'
+export const METADATA_VIEW_FEATURE_STATE_KEY = 'metadataView'
-export interface MdViewState {
+export interface MetadataViewState {
loadingFull: boolean
error: { notFound?: boolean; otherError?: string } | null
metadata?: Partial
related?: CatalogRecord[]
+ userFeedbacks?: UserFeedback[]
+ allUserFeedbacksLoading: boolean
+ addUserFeedbackLoading: boolean
chartConfig?: DatavizConfigurationModel
}
-export const initialMdviewState: MdViewState = {
+export const initialMetadataViewState: MetadataViewState = {
error: null,
loadingFull: false,
+ allUserFeedbacksLoading: false,
+ addUserFeedbackLoading: false,
}
-const mdViewReducer = createReducer(
- initialMdviewState,
- on(MdViewActions.loadFullMetadata, (state) => ({
+const metadataViewReducer = createReducer(
+ initialMetadataViewState,
+
+ /*
+ Metadata reducers
+ */
+ on(MetadataViewActions.loadFullMetadata, (state) => ({
...state,
error: null,
loadingFull: true,
})),
- on(MdViewActions.setIncompleteMetadata, (state, { incomplete }) => ({
+ on(MetadataViewActions.setIncompleteMetadata, (state, { incomplete }) => ({
...state,
error: null,
metadata: incomplete,
})),
- on(MdViewActions.loadFullSuccess, (state, { full }) => ({
+ on(MetadataViewActions.loadFullMetadataSuccess, (state, { full }) => ({
...state,
error: null,
metadata: full,
loadingFull: false,
})),
- on(MdViewActions.loadFullFailure, (state, { otherError, notFound }) => ({
- ...state,
- error: { otherError, notFound },
- loadingFull: false,
- })),
- on(MdViewActions.setRelated, (state, { related }) => ({
+ on(
+ MetadataViewActions.loadFullMetadataFailure,
+ (state, { otherError, notFound }) => ({
+ ...state,
+ error: { otherError, notFound },
+ loadingFull: false,
+ })
+ ),
+ on(MetadataViewActions.closeMetadata, (state) => {
+ const { metadata, related, userFeedbacks, ...stateWithoutMetadata } = state
+ return stateWithoutMetadata
+ }),
+
+ /*
+ Related reducers
+ */
+ on(MetadataViewActions.setRelated, (state, { related }) => ({
...state,
related,
})),
- on(MdViewActions.setChartConfig, (state, { chartConfig }) => ({
+
+ /*
+ ChartConfig reducers
+ */
+ on(MetadataViewActions.setChartConfig, (state, { chartConfig }) => ({
...state,
chartConfig,
})),
- on(MdViewActions.close, (state) => {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const { metadata, related, ...stateWithoutMd } = state
- return stateWithoutMd
- })
+
+ /*
+ UserFeedbacks reducers
+ */
+ on(MetadataViewActions.loadUserFeedbacks, (state) => ({
+ ...state,
+ error: null,
+ allUserFeedbacksLoading: true,
+ })),
+ on(MetadataViewActions.addUserFeedback, (state) => ({
+ ...state,
+ addUserFeedbackLoading: true,
+ })),
+ on(
+ MetadataViewActions.loadUserFeedbacksSuccess,
+ (state, { userFeedbacks }) => ({
+ ...state,
+ error: null,
+ userFeedbacks: userFeedbacks,
+ addUserFeedbackLoading: false,
+ allUserFeedbacksLoading: false,
+ })
+ ),
+ on(
+ MetadataViewActions.loadUserFeedbacksFailure,
+ (state, { otherError, notFound }) => ({
+ ...state,
+ error: { otherError, notFound },
+ addUserFeedbackLoading: false,
+ allUserFeedbacksLoading: false,
+ })
+ )
)
-export function reducer(state: MdViewState | undefined, action: Action) {
- return mdViewReducer(state, action)
+export function reducer(
+ metadataViewState: MetadataViewState | undefined,
+ action: Action
+) {
+ return metadataViewReducer(metadataViewState, action)
}
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 54b9ef8f59..23524200d6 100644
--- a/libs/feature/record/src/lib/state/mdview.selectors.spec.ts
+++ b/libs/feature/record/src/lib/state/mdview.selectors.spec.ts
@@ -34,20 +34,25 @@ describe('MdView Selectors', () => {
const results = MdViewSelectors.getMetadataUuid.projector(state)
expect(results).toBe('321321321321')
})
+
it('returns null if no metadata in the state', () => {
const results = MdViewSelectors.getMetadataUuid.projector({
loadingFull: false,
error: null,
+ allUserFeedbacksLoading: false,
+ addUserFeedbackLoading: false,
})
expect(results).toBe(null)
})
})
+
describe('getMetadata', () => {
it('returns the metadata in the state', () => {
const results = MdViewSelectors.getMetadata.projector(state)
expect(results).toBe(state.metadata)
})
})
+
describe('getMetadataIsIncomplete', () => {
it('returns true when incomplete', () => {
const results = MdViewSelectors.getMetadataIsIncomplete.projector({
@@ -56,23 +61,29 @@ describe('MdView Selectors', () => {
})
expect(results).toBe(true)
})
+
it('returns false when complete', () => {
const results = MdViewSelectors.getMetadataIsIncomplete.projector(state)
expect(results).toBe(false)
})
+
it('returns null if no metadata', () => {
const results = MdViewSelectors.getMetadataIsIncomplete.projector({
loadingFull: false,
error: null,
+ allUserFeedbacksLoading: false,
+ addUserFeedbackLoading: false,
})
expect(results).toBe(null)
})
})
+
describe('getMetadataIsLoading', () => {
it('returns false if not loading', () => {
const results = MdViewSelectors.getMetadataIsLoading.projector(state)
expect(results).toBe(false)
})
+
it('returns true if loading', () => {
const results = MdViewSelectors.getMetadataIsLoading.projector({
...state,
@@ -81,6 +92,7 @@ describe('MdView Selectors', () => {
expect(results).toBe(true)
})
})
+
describe('getMetadataError', () => {
it('returns error if present', () => {
const results = MdViewSelectors.getMetadataError.projector({
@@ -89,14 +101,18 @@ describe('MdView Selectors', () => {
})
expect(results).toBe('ouch')
})
+
it('returns null if no error', () => {
const results = MdViewSelectors.getMetadataError.projector({
loadingFull: false,
error: null,
+ allUserFeedbacksLoading: false,
+ addUserFeedbackLoading: false,
})
expect(results).toBe(null)
})
})
+
describe('getRelated', () => {
it('returns related records', () => {
const results = MdViewSelectors.getRelated.projector({
@@ -106,6 +122,7 @@ describe('MdView Selectors', () => {
expect(results).toEqual([relatedRecord])
})
})
+
describe('getChartConfig', () => {
it('returns chart config', () => {
const results = MdViewSelectors.getChartConfig.projector({
diff --git a/libs/feature/record/src/lib/state/mdview.selectors.ts b/libs/feature/record/src/lib/state/mdview.selectors.ts
index ee79e613be..bb035e12a7 100644
--- a/libs/feature/record/src/lib/state/mdview.selectors.ts
+++ b/libs/feature/record/src/lib/state/mdview.selectors.ts
@@ -1,36 +1,66 @@
import { createFeatureSelector, createSelector } from '@ngrx/store'
-import { MD_VIEW_FEATURE_STATE_KEY, MdViewState } from './mdview.reducer'
+import {
+ METADATA_VIEW_FEATURE_STATE_KEY,
+ MetadataViewState,
+} from './mdview.reducer'
-export const getMdViewState = createFeatureSelector(
- MD_VIEW_FEATURE_STATE_KEY
+export const getMdViewState = createFeatureSelector(
+ METADATA_VIEW_FEATURE_STATE_KEY
)
+/*
+ Metadata selectors
+*/
export const getMetadataUuid = createSelector(
getMdViewState,
- (state: MdViewState) =>
+ (state: MetadataViewState) =>
state.metadata ? state.metadata.uniqueIdentifier : null
)
export const getMetadata = createSelector(
getMdViewState,
- (state: MdViewState) => state.metadata
+ (state: MetadataViewState) => state.metadata
)
export const getMetadataIsIncomplete = createSelector(
getMdViewState,
- (state: MdViewState) => (state.metadata ? state.loadingFull : null)
+ (state: MetadataViewState) => (state.metadata ? state.loadingFull : null)
)
export const getMetadataIsLoading = createSelector(
getMdViewState,
- (state: MdViewState) => state.loadingFull
+ (state: MetadataViewState) => state.loadingFull
)
export const getMetadataError = createSelector(
getMdViewState,
- (state: MdViewState) => state.error
+ (state: MetadataViewState) => state.error
)
+
+/*
+ Related selectors
+*/
export const getRelated = createSelector(
getMdViewState,
- (state: MdViewState) => state.related
+ (state: MetadataViewState) => state.related
)
+
+/*
+ Metadata selectors
+*/
export const getChartConfig = createSelector(
getMdViewState,
- (state: MdViewState) => state.chartConfig
+ (state: MetadataViewState) => state.chartConfig
+)
+
+/*
+ UserFeedback selectors
+*/
+export const getUserFeedbacks = createSelector(
+ getMdViewState,
+ (state: MetadataViewState) => state.userFeedbacks
+)
+export const getAllUserFeedbacksLoading = createSelector(
+ getMdViewState,
+ (state: MetadataViewState) => state.allUserFeedbacksLoading
+)
+export const getAddUserFeedbacksLoading = createSelector(
+ getMdViewState,
+ (state: MetadataViewState) => state.addUserFeedbackLoading
)
diff --git a/libs/feature/router/src/lib/default/state/router.effects.spec.ts b/libs/feature/router/src/lib/default/state/router.effects.spec.ts
index b2faf92c08..4936c377e5 100644
--- a/libs/feature/router/src/lib/default/state/router.effects.spec.ts
+++ b/libs/feature/router/src/lib/default/state/router.effects.spec.ts
@@ -165,7 +165,7 @@ describe('RouterEffects', () => {
} as any),
})
const expected = hot('-a', {
- a: MdViewActions.close(),
+ a: MdViewActions.closeMetadata(),
})
expect(effects.navigateToSearch$).toBeObservable(expected)
})
diff --git a/libs/feature/router/src/lib/default/state/router.effects.ts b/libs/feature/router/src/lib/default/state/router.effects.ts
index 822b6c9a2b..19a95c8f8c 100644
--- a/libs/feature/router/src/lib/default/state/router.effects.ts
+++ b/libs/feature/router/src/lib/default/state/router.effects.ts
@@ -15,7 +15,7 @@ import {
} from '@geonetwork-ui/common/domain/model/search'
import { Actions, createEffect, ofType } from '@ngrx/effects'
import { navigation } from '@ngrx/router-store/data-persistence'
-import { of, pairwise, startWith, withLatestFrom } from 'rxjs'
+import { of, pairwise, startWith } from 'rxjs'
import { map, mergeMap, tap } from 'rxjs/operators'
import * as RouterActions from './router.actions'
import { RouterFacade } from './router.facade'
@@ -141,7 +141,7 @@ export class RouterEffects {
navigateToSearch$ = createEffect(() =>
this._actions$.pipe(
navigation(this.routerConfig.searchRouteComponent, {
- run: () => MdViewActions.close(),
+ run: () => MdViewActions.closeMetadata(),
})
)
)
diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts
index d2f7f9d2ab..64373c318c 100644
--- a/libs/ui/elements/src/lib/ui-elements.module.ts
+++ b/libs/ui/elements/src/lib/ui-elements.module.ts
@@ -31,6 +31,8 @@ import { MaxLinesComponent } from './max-lines/max-lines.component'
import { RecordApiFormComponent } from './record-api-form/record-api-form.component'
import { MarkdownParserComponent } from './markdown-parser/markdown-parser.component'
import { ImageOverlayPreviewComponent } from './image-overlay-preview/image-overlay-preview.component'
+import { UserFeedbackItemComponent } from './user-feedback-item/user-feedback-item.component'
+import { TimeSincePipe } from './user-feedback-item/time-since.pipe'
@NgModule({
imports: [
@@ -47,6 +49,7 @@ import { ImageOverlayPreviewComponent } from './image-overlay-preview/image-over
NgOptimizedImage,
MarkdownParserComponent,
ThumbnailComponent,
+ TimeSincePipe,
],
declarations: [
MetadataInfoComponent,
@@ -68,6 +71,7 @@ import { ImageOverlayPreviewComponent } from './image-overlay-preview/image-over
PaginationButtonsComponent,
MaxLinesComponent,
RecordApiFormComponent,
+ UserFeedbackItemComponent,
ImageOverlayPreviewComponent,
],
exports: [
@@ -91,6 +95,7 @@ import { ImageOverlayPreviewComponent } from './image-overlay-preview/image-over
MaxLinesComponent,
RecordApiFormComponent,
MarkdownParserComponent,
+ UserFeedbackItemComponent,
ImageOverlayPreviewComponent,
],
})
diff --git a/libs/ui/elements/src/lib/user-feedback-item/time-since.pipe.ts b/libs/ui/elements/src/lib/user-feedback-item/time-since.pipe.ts
new file mode 100644
index 0000000000..ae604a420e
--- /dev/null
+++ b/libs/ui/elements/src/lib/user-feedback-item/time-since.pipe.ts
@@ -0,0 +1,54 @@
+import { Pipe, PipeTransform } from '@angular/core'
+import { TranslateService } from '@ngx-translate/core'
+import { formatDistance } from 'date-fns'
+import { de, enUS, es, fr, it, nl, pt, sk } from 'date-fns/locale'
+
+@Pipe({
+ name: 'timeSince',
+ standalone: true,
+})
+export class TimeSincePipe implements PipeTransform {
+ constructor(private translate: TranslateService) {}
+
+ transform(value: Date): string {
+ if (isNaN(value.getTime())) {
+ throw new Error('Invalid Date')
+ }
+
+ const maintenant = new Date()
+ let locale: Locale
+
+ switch (this.translate.currentLang) {
+ case 'fr':
+ locale = fr
+ break
+ case 'de':
+ locale = de
+ break
+ case 'es':
+ locale = es
+ break
+ case 'it':
+ locale = it
+ break
+ case 'nl':
+ locale = nl
+ break
+ case 'pt':
+ locale = pt
+ break
+ case 'sk':
+ locale = sk
+ break
+ case 'en':
+ default:
+ locale = enUS
+ break
+ }
+
+ return formatDistance(value, maintenant, {
+ addSuffix: true,
+ locale: locale,
+ })
+ }
+}
diff --git a/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.css b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.css
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.html b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.html
new file mode 100644
index 0000000000..f91bd17d7e
--- /dev/null
+++ b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+ {{ userFeedbackParent.authorName }}
+ {{ userFeedbackParent.date | timeSince }}
+
+
+
+ {{ userFeedbackParent.comment }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.spec.ts b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.spec.ts
new file mode 100644
index 0000000000..bb347a200f
--- /dev/null
+++ b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.spec.ts
@@ -0,0 +1,63 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import { TranslateModule } from '@ngx-translate/core'
+import { UserFeedbackItemComponent } from './user-feedback-item.component'
+import { ChangeDetectionStrategy } from '@angular/core'
+import { SOME_USER_FEEDBACKS } from '@geonetwork-ui/common/fixtures'
+import { TimeSincePipe } from './time-since.pipe'
+
+describe('UserFeedbackItemComponent', () => {
+ let component: UserFeedbackItemComponent
+ let fixture: ComponentFixture
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [UserFeedbackItemComponent],
+ imports: [TranslateModule.forRoot(), TimeSincePipe],
+ })
+ .overrideComponent(UserFeedbackItemComponent, {
+ set: { changeDetection: ChangeDetectionStrategy.Default },
+ })
+ .compileComponents()
+ })
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserFeedbackItemComponent)
+ component = fixture.componentInstance
+ component.userFeedbackParent = { ...SOME_USER_FEEDBACKS[0], avatarUrl: '' }
+ fixture.detectChanges()
+ })
+
+ it('should create', () => {
+ expect(component).toBeTruthy()
+ })
+
+ describe('publishNewAnswer()', () => {
+ it('should not emit new answer if new answer is empty', () => {
+ component.newAnswer = ''
+ component.newUserFeedbackAnswer.emit = jest.fn()
+ component.publishNewAnswer()
+ expect(component.newUserFeedbackAnswer.emit).not.toHaveBeenCalled()
+ })
+
+ it('should emit new answer if new answer is not empty', () => {
+ component.newAnswer = 'This is a new answer'
+ component.newUserFeedbackAnswer.emit = jest.fn()
+ component.publishNewAnswer()
+ expect(component.newUserFeedbackAnswer.emit).toHaveBeenCalled()
+ })
+ })
+
+ describe('onNewAnswerValueChange()', () => {
+ it('should set isAnswerEmpty to true if new answer is empty', () => {
+ component.newAnswer = ''
+ component.onNewAnswerValueChange()
+ expect(component.isAnswerEmpty).toBe(true)
+ })
+
+ it('should set isAnswerEmpty to false if new answer is not empty', () => {
+ component.newAnswer = 'This is a new answer'
+ component.onNewAnswerValueChange()
+ expect(component.isAnswerEmpty).toBe(false)
+ })
+ })
+})
diff --git a/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.stories.ts b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.stories.ts
new file mode 100644
index 0000000000..a2803a7a04
--- /dev/null
+++ b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.stories.ts
@@ -0,0 +1,99 @@
+import {
+ applicationConfig,
+ componentWrapperDecorator,
+ moduleMetadata,
+ StoryObj,
+} from '@storybook/angular'
+import { CommonModule } from '@angular/common'
+import { UserFeedbackItemComponent } from './user-feedback-item.component'
+import { TranslateModule } from '@ngx-translate/core'
+import {
+ TRANSLATE_DEFAULT_CONFIG,
+ UtilI18nModule,
+} from '@geonetwork-ui/util/i18n'
+import { TimeSincePipe } from './time-since.pipe'
+import { HttpClientModule } from '@angular/common/http'
+import { importProvidersFrom } from '@angular/core'
+import { ButtonComponent, TextAreaComponent } from '@geonetwork-ui/ui/inputs'
+import { SpinningLoaderComponent } from '@geonetwork-ui/ui/widgets'
+
+export default {
+ title: 'Elements/UserFeedbackItemComponent',
+ component: UserFeedbackItemComponent,
+ decorators: [
+ moduleMetadata({
+ declarations: [UserFeedbackItemComponent, SpinningLoaderComponent],
+ imports: [
+ CommonModule,
+ UtilI18nModule,
+ TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG),
+ TimeSincePipe,
+ TextAreaComponent,
+ ButtonComponent,
+ ],
+ }),
+ applicationConfig({
+ providers: [importProvidersFrom(HttpClientModule)],
+ }),
+ componentWrapperDecorator(
+ (story) => `${story}
`
+ ),
+ ],
+}
+export const Primary: StoryObj = {
+ args: {
+ userFeedbackParent: {
+ published: true,
+ avatarUrl:
+ 'https://hips.hearstapps.com/hmg-prod/images/red-small-german-spitz-walking-in-the-autumn-park-royalty-free-image-1580496879.jpg?crop=0.670xw:1.00xh;0.173xw,0&resize=75:*',
+ authorName: 'John Doe',
+ date: new Date('2024-01-01T00:00:00Z'),
+ comment: 'Sample comment',
+ parentUuid: null,
+ uuid: '1',
+ metadataUUID: '',
+ authorUserId: '',
+ authorEmail: '',
+ },
+ userFeedBacksAnswers: [
+ {
+ published: true,
+ avatarUrl:
+ 'https://wp.inews.co.uk/wp-content/uploads/2019/06/Papageitaucher_Fratercula_arctica-1.jpg?resize=67,67&strip=all&quality=90',
+ authorName: 'Maria Carmen',
+ date: new Date('2024-03-30T00:00:00Z'),
+ comment: 'Sample answer number one',
+ parentUuid: '1',
+ uuid: '',
+ metadataUUID: '',
+ authorUserId: '',
+ authorEmail: '',
+ },
+ {
+ published: true,
+ avatarUrl:
+ 'https://resize.prod.femina.ladmedia.fr/rblr/80,80/img/var/2023-07/mourir-peut-attendre.jpg',
+ authorName: 'James Bond',
+ date: new Date('2024-04-18T00:00:00Z'),
+ comment: 'Sample answer number two',
+ parentUuid: '1',
+ uuid: '',
+ metadataUUID: '',
+ authorUserId: '',
+ authorEmail: '',
+ },
+ ],
+ isActiveUserEditor: true,
+ activeUser: {
+ id: '1',
+ email: 'john@example.com',
+ name: 'John',
+ surname: 'Doe',
+ profile: 'ADMIN',
+ username: '',
+ organisation: '',
+ },
+ isLastComment: true,
+ isAddUserFeedbackLoading: false,
+ },
+}
diff --git a/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.ts b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.ts
new file mode 100644
index 0000000000..1f1e50238b
--- /dev/null
+++ b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.ts
@@ -0,0 +1,63 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ EventEmitter,
+ Input,
+ OnInit,
+ Output,
+} from '@angular/core'
+import {
+ UserFeedback,
+ UserFeedbackViewModel,
+} from '@geonetwork-ui/common/domain/model/record'
+import { UserModel } from '@geonetwork-ui/common/domain/model/user'
+
+@Component({
+ selector: 'gn-ui-user-feedback-item',
+ templateUrl: './user-feedback-item.component.html',
+ styleUrls: ['./user-feedback-item.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class UserFeedbackItemComponent implements OnInit {
+ @Input() userFeedbackParent: UserFeedbackViewModel
+ @Input() userFeedBacksAnswers: UserFeedbackViewModel[]
+ @Input() isActiveUserEditor: boolean
+ @Input() activeUser: UserModel
+ @Input() isLastComment: boolean
+ @Input() isAddUserFeedbackLoading: boolean
+
+ @Output() newUserFeedbackAnswer = new EventEmitter()
+
+ isAnAnswer = false
+ newAnswer = ''
+ isAnswerEmpty = true
+
+ ngOnInit(): void {
+ this.isAnAnswer = !!this.userFeedbackParent.parentUuid
+ }
+
+ onNewAnswerValueChange() {
+ this.isAnswerEmpty = this.newAnswer.length === 0
+ }
+
+ publishNewAnswer() {
+ if (this.newAnswer.trim() === '') return
+
+ const newAnswer: UserFeedback = {
+ ...this.userFeedbackParent,
+ uuid: undefined,
+ published: true,
+ comment: this.newAnswer,
+ parentUuid: this.userFeedbackParent.uuid,
+ authorUserId: this.activeUser?.id,
+ authorEmail: this.activeUser?.email,
+ date: new Date(),
+ authorName: `${this.activeUser?.name} ${this.activeUser?.surname}`,
+ }
+
+ this.newUserFeedbackAnswer.emit(newAnswer)
+
+ this.newAnswer = ''
+ this.onNewAnswerValueChange()
+ }
+}
diff --git a/libs/ui/inputs/src/lib/text-area/text-area.component.html b/libs/ui/inputs/src/lib/text-area/text-area.component.html
index a1fdbf4de0..8e7e86164d 100644
--- a/libs/ui/inputs/src/lib/text-area/text-area.component.html
+++ b/libs/ui/inputs/src/lib/text-area/text-area.component.html
@@ -2,11 +2,12 @@
diff --git a/libs/ui/inputs/src/lib/text-area/text-area.component.ts b/libs/ui/inputs/src/lib/text-area/text-area.component.ts
index 758bf7e53b..a6c25f7357 100644
--- a/libs/ui/inputs/src/lib/text-area/text-area.component.ts
+++ b/libs/ui/inputs/src/lib/text-area/text-area.component.ts
@@ -15,7 +15,12 @@ import { distinctUntilChanged } from 'rxjs/operators'
standalone: true,
})
export class TextAreaComponent implements AfterViewInit {
+ private readonly baseClasses: string
+ private readonly disabledClasses: string
+
@Input() value = ''
+ @Input() disabled = false
+ @Input() extraClass = ''
@Input() placeholder: string
@Input() required = false
@@ -24,6 +29,30 @@ export class TextAreaComponent implements AfterViewInit {
@ViewChild('input') input
+ constructor() {
+ this.baseClasses = [
+ 'w-full',
+ 'pt-2',
+ 'pl-2',
+ 'resize-none',
+ 'border',
+ 'border-gray-800',
+ 'rounded italic',
+ 'leading-tight',
+ 'focus:outline-none',
+ 'focus:bg-background',
+ 'focus:border-primary',
+ ].join(' ')
+
+ this.disabledClasses = ['cursor-not-allowed'].join(' ')
+ }
+
+ get classList() {
+ return `${this.baseClasses} ${this.extraClass} ${
+ this.disabled ? this.disabledClasses : ''
+ }`
+ }
+
ngAfterViewInit() {
this.checkRequired(this.input.nativeElement.value)
}
diff --git a/libs/ui/inputs/src/lib/text-input/text-input.component.html b/libs/ui/inputs/src/lib/text-input/text-input.component.html
index ee459de3f8..df92d6bbb3 100644
--- a/libs/ui/inputs/src/lib/text-input/text-input.component.html
+++ b/libs/ui/inputs/src/lib/text-input/text-input.component.html
@@ -1,6 +1,6 @@
()
@Output() valueChange = this.rawChange.pipe(distinctUntilChanged())
-
@ViewChild('input') input
+ get classList() {
+ return `${this.baseClass} ${this.extraClass}`
+ }
+
ngAfterViewInit() {
this.checkRequired(this.input.nativeElement.value)
}
diff --git a/package-lock.json b/package-lock.json
index 91d3460244..27b0b00c67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"basiclightbox": "^5.0.4",
"chart.js": "^4.2.0",
"chroma-js": "^2.1.2",
- "date-fns": "^2.29.3",
+ "date-fns": "^2.30.0",
"document-register-element": "^1.14.10",
"duration-relativetimeformat": "^2.0.3",
"embla-carousel": "^8.0.0-rc14",
@@ -103,6 +103,7 @@
"@storybook/addon-essentials": "7.2.1",
"@storybook/angular": "7.2.1",
"@types/chroma-js": "^2.1.3",
+ "@types/date-fns": "^2.6.0",
"@types/express": "^4.17.12",
"@types/geojson": "^7946.0.7",
"@types/jest": "^29.4.0",
@@ -4489,15 +4490,15 @@
}
},
"node_modules/@jest/console": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.2.tgz",
- "integrity": "sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz",
+ "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==",
"dependencies": {
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"@types/node": "*",
"chalk": "^4.0.0",
- "jest-message-util": "^29.6.2",
- "jest-util": "^29.6.2",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
"slash": "^3.0.0"
},
"engines": {
@@ -4652,67 +4653,67 @@
}
},
"node_modules/@jest/environment": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.2.tgz",
- "integrity": "sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz",
+ "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==",
"dependencies": {
- "@jest/fake-timers": "^29.6.2",
- "@jest/types": "^29.6.1",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/types": "^29.6.3",
"@types/node": "*",
- "jest-mock": "^29.6.2"
+ "jest-mock": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/expect": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.2.tgz",
- "integrity": "sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==",
"dependencies": {
- "expect": "^29.6.2",
- "jest-snapshot": "^29.6.2"
+ "expect": "^29.7.0",
+ "jest-snapshot": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/expect-utils": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.2.tgz",
- "integrity": "sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
+ "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
"dependencies": {
- "jest-get-type": "^29.4.3"
+ "jest-get-type": "^29.6.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/fake-timers": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.2.tgz",
- "integrity": "sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz",
+ "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==",
"dependencies": {
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"@sinonjs/fake-timers": "^10.0.2",
"@types/node": "*",
- "jest-message-util": "^29.6.2",
- "jest-mock": "^29.6.2",
- "jest-util": "^29.6.2"
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-util": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/globals": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.2.tgz",
- "integrity": "sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
+ "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
"dependencies": {
- "@jest/environment": "^29.6.2",
- "@jest/expect": "^29.6.2",
- "@jest/types": "^29.6.1",
- "jest-mock": "^29.6.2"
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "jest-mock": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -4809,9 +4810,9 @@
}
},
"node_modules/@jest/schemas": {
- "version": "29.6.0",
- "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz",
- "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==",
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
+ "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
"dependencies": {
"@sinclair/typebox": "^0.27.8"
},
@@ -4820,9 +4821,9 @@
}
},
"node_modules/@jest/source-map": {
- "version": "29.6.0",
- "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.0.tgz",
- "integrity": "sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==",
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz",
+ "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.18",
"callsites": "^3.0.0",
@@ -4833,12 +4834,12 @@
}
},
"node_modules/@jest/test-result": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.2.tgz",
- "integrity": "sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz",
+ "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==",
"dependencies": {
- "@jest/console": "^29.6.2",
- "@jest/types": "^29.6.1",
+ "@jest/console": "^29.7.0",
+ "@jest/types": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
"collect-v8-coverage": "^1.0.0"
},
@@ -4861,21 +4862,21 @@
}
},
"node_modules/@jest/transform": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.2.tgz",
- "integrity": "sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
+ "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
"dependencies": {
"@babel/core": "^7.11.6",
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"@jridgewell/trace-mapping": "^0.3.18",
"babel-plugin-istanbul": "^6.1.1",
"chalk": "^4.0.0",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.9",
- "jest-haste-map": "^29.6.2",
- "jest-regex-util": "^29.4.3",
- "jest-util": "^29.6.2",
+ "jest-haste-map": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
"micromatch": "^4.0.4",
"pirates": "^4.0.4",
"slash": "^3.0.0",
@@ -4939,11 +4940,11 @@
}
},
"node_modules/@jest/types": {
- "version": "29.6.1",
- "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz",
- "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==",
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
+ "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"dependencies": {
- "@jest/schemas": "^29.6.0",
+ "@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
@@ -12246,6 +12247,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/date-fns": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.6.0.tgz",
+ "integrity": "sha512-9DSw2ZRzV0Tmpa6PHHJbMcZn79HHus+BBBohcOaDzkK/G3zMjDUDYjJIWBFLbkh+1+/IOS0A59BpQfdr37hASg==",
+ "deprecated": "This is a stub types definition for date-fns (https://github.com/date-fns/date-fns). date-fns provides its own type definitions, so you don't need @types/date-fns installed!",
+ "dev": true,
+ "dependencies": {
+ "date-fns": "*"
+ }
+ },
"node_modules/@types/detect-port": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/detect-port/-/detect-port-1.3.3.tgz",
@@ -12376,9 +12387,9 @@
}
},
"node_modules/@types/jest": {
- "version": "29.5.3",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.3.tgz",
- "integrity": "sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==",
+ "version": "29.5.12",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz",
+ "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==",
"dev": true,
"dependencies": {
"expect": "^29.0.0",
@@ -17631,9 +17642,9 @@
}
},
"node_modules/diff-sequences": {
- "version": "29.4.3",
- "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz",
- "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==",
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
+ "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
@@ -18627,16 +18638,15 @@
}
},
"node_modules/expect": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.2.tgz",
- "integrity": "sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
"dependencies": {
- "@jest/expect-utils": "^29.6.2",
- "@types/node": "*",
- "jest-get-type": "^29.4.3",
- "jest-matcher-utils": "^29.6.2",
- "jest-message-util": "^29.6.2",
- "jest-util": "^29.6.2"
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -22102,14 +22112,14 @@
}
},
"node_modules/jest-diff": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.2.tgz",
- "integrity": "sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
+ "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
"dependencies": {
"chalk": "^4.0.0",
- "diff-sequences": "^29.4.3",
- "jest-get-type": "^29.4.3",
- "pretty-format": "^29.6.2"
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -22175,15 +22185,15 @@
}
},
"node_modules/jest-each": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.2.tgz",
- "integrity": "sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz",
+ "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==",
"dependencies": {
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"chalk": "^4.0.0",
- "jest-get-type": "^29.4.3",
- "jest-util": "^29.6.2",
- "pretty-format": "^29.6.2"
+ "jest-get-type": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "pretty-format": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -22397,27 +22407,27 @@
}
},
"node_modules/jest-get-type": {
- "version": "29.4.3",
- "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz",
- "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==",
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
+ "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-haste-map": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.2.tgz",
- "integrity": "sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz",
+ "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==",
"dependencies": {
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"@types/graceful-fs": "^4.1.3",
"@types/node": "*",
"anymatch": "^3.0.3",
"fb-watchman": "^2.0.0",
"graceful-fs": "^4.2.9",
- "jest-regex-util": "^29.4.3",
- "jest-util": "^29.6.2",
- "jest-worker": "^29.6.2",
+ "jest-regex-util": "^29.6.3",
+ "jest-util": "^29.7.0",
+ "jest-worker": "^29.7.0",
"micromatch": "^4.0.4",
"walker": "^1.0.8"
},
@@ -22441,14 +22451,14 @@
}
},
"node_modules/jest-matcher-utils": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz",
- "integrity": "sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
+ "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
"dependencies": {
"chalk": "^4.0.0",
- "jest-diff": "^29.6.2",
- "jest-get-type": "^29.4.3",
- "pretty-format": "^29.6.2"
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -22503,17 +22513,17 @@
}
},
"node_modules/jest-message-util": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.2.tgz",
- "integrity": "sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
+ "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
"dependencies": {
"@babel/code-frame": "^7.12.13",
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"@types/stack-utils": "^2.0.0",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"micromatch": "^4.0.4",
- "pretty-format": "^29.6.2",
+ "pretty-format": "^29.7.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.3"
},
@@ -22570,13 +22580,13 @@
}
},
"node_modules/jest-mock": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.2.tgz",
- "integrity": "sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz",
+ "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==",
"dependencies": {
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"@types/node": "*",
- "jest-util": "^29.6.2"
+ "jest-util": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -22627,24 +22637,24 @@
}
},
"node_modules/jest-regex-util": {
- "version": "29.4.3",
- "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz",
- "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==",
+ "version": "29.6.3",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz",
+ "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==",
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-resolve": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.2.tgz",
- "integrity": "sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz",
+ "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==",
"dependencies": {
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
- "jest-haste-map": "^29.6.2",
+ "jest-haste-map": "^29.7.0",
"jest-pnp-resolver": "^1.2.2",
- "jest-util": "^29.6.2",
- "jest-validate": "^29.6.2",
+ "jest-util": "^29.7.0",
+ "jest-validate": "^29.7.0",
"resolve": "^1.20.0",
"resolve.exports": "^2.0.0",
"slash": "^3.0.0"
@@ -22819,30 +22829,30 @@
}
},
"node_modules/jest-runtime": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.2.tgz",
- "integrity": "sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg==",
- "dependencies": {
- "@jest/environment": "^29.6.2",
- "@jest/fake-timers": "^29.6.2",
- "@jest/globals": "^29.6.2",
- "@jest/source-map": "^29.6.0",
- "@jest/test-result": "^29.6.2",
- "@jest/transform": "^29.6.2",
- "@jest/types": "^29.6.1",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz",
+ "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/fake-timers": "^29.7.0",
+ "@jest/globals": "^29.7.0",
+ "@jest/source-map": "^29.6.3",
+ "@jest/test-result": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
"@types/node": "*",
"chalk": "^4.0.0",
"cjs-module-lexer": "^1.0.0",
"collect-v8-coverage": "^1.0.0",
"glob": "^7.1.3",
"graceful-fs": "^4.2.9",
- "jest-haste-map": "^29.6.2",
- "jest-message-util": "^29.6.2",
- "jest-mock": "^29.6.2",
- "jest-regex-util": "^29.4.3",
- "jest-resolve": "^29.6.2",
- "jest-snapshot": "^29.6.2",
- "jest-util": "^29.6.2",
+ "jest-haste-map": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-mock": "^29.7.0",
+ "jest-regex-util": "^29.6.3",
+ "jest-resolve": "^29.7.0",
+ "jest-snapshot": "^29.7.0",
+ "jest-util": "^29.7.0",
"slash": "^3.0.0",
"strip-bom": "^4.0.0"
},
@@ -22899,29 +22909,29 @@
}
},
"node_modules/jest-snapshot": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.2.tgz",
- "integrity": "sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
+ "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
"dependencies": {
"@babel/core": "^7.11.6",
"@babel/generator": "^7.7.2",
"@babel/plugin-syntax-jsx": "^7.7.2",
"@babel/plugin-syntax-typescript": "^7.7.2",
"@babel/types": "^7.3.3",
- "@jest/expect-utils": "^29.6.2",
- "@jest/transform": "^29.6.2",
- "@jest/types": "^29.6.1",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
"babel-preset-current-node-syntax": "^1.0.0",
"chalk": "^4.0.0",
- "expect": "^29.6.2",
+ "expect": "^29.7.0",
"graceful-fs": "^4.2.9",
- "jest-diff": "^29.6.2",
- "jest-get-type": "^29.4.3",
- "jest-matcher-utils": "^29.6.2",
- "jest-message-util": "^29.6.2",
- "jest-util": "^29.6.2",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
"natural-compare": "^1.4.0",
- "pretty-format": "^29.6.2",
+ "pretty-format": "^29.7.0",
"semver": "^7.5.3"
},
"engines": {
@@ -22977,11 +22987,11 @@
}
},
"node_modules/jest-util": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.2.tgz",
- "integrity": "sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
+ "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
"dependencies": {
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
@@ -23041,16 +23051,16 @@
}
},
"node_modules/jest-validate": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.2.tgz",
- "integrity": "sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz",
+ "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==",
"dependencies": {
- "@jest/types": "^29.6.1",
+ "@jest/types": "^29.6.3",
"camelcase": "^6.2.0",
"chalk": "^4.0.0",
- "jest-get-type": "^29.4.3",
+ "jest-get-type": "^29.6.3",
"leven": "^3.1.0",
- "pretty-format": "^29.6.2"
+ "pretty-format": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
@@ -23182,12 +23192,12 @@
}
},
"node_modules/jest-worker": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.2.tgz",
- "integrity": "sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz",
+ "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==",
"dependencies": {
"@types/node": "*",
- "jest-util": "^29.6.2",
+ "jest-util": "^29.7.0",
"merge-stream": "^2.0.0",
"supports-color": "^8.0.0"
},
@@ -27744,11 +27754,11 @@
}
},
"node_modules/pretty-format": {
- "version": "29.6.2",
- "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz",
- "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==",
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
+ "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dependencies": {
- "@jest/schemas": "^29.6.0",
+ "@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
diff --git a/package.json b/package.json
index c47e4b6396..dabc795ef1 100644
--- a/package.json
+++ b/package.json
@@ -79,7 +79,7 @@
"basiclightbox": "^5.0.4",
"chart.js": "^4.2.0",
"chroma-js": "^2.1.2",
- "date-fns": "^2.29.3",
+ "date-fns": "^2.30.0",
"document-register-element": "^1.14.10",
"duration-relativetimeformat": "^2.0.3",
"embla-carousel": "^8.0.0-rc14",
@@ -138,6 +138,7 @@
"@storybook/addon-essentials": "7.2.1",
"@storybook/angular": "7.2.1",
"@types/chroma-js": "^2.1.3",
+ "@types/date-fns": "^2.6.0",
"@types/express": "^4.17.12",
"@types/geojson": "^7946.0.7",
"@types/jest": "^29.4.0",
diff --git a/support-services/docker-entrypoint-initdb.d/dump b/support-services/docker-entrypoint-initdb.d/dump
index 7bef8d3978..2778ff3395 100644
Binary files a/support-services/docker-entrypoint-initdb.d/dump and b/support-services/docker-entrypoint-initdb.d/dump differ
diff --git a/translations/de.json b/translations/de.json
index a1bd4ead9d..62b229bf04 100644
--- a/translations/de.json
+++ b/translations/de.json
@@ -1,4 +1,5 @@
{
+ "button.login": "",
"catalog.figures.datasets": "{count, plural, =0{Datensätze} one{Datensatz} other{Datensätze}}",
"catalog.figures.organisations": "{count, plural, =0{Organisationen} one{Organisation} other{Organisationen}}",
"chart.aggregation.average": "Durchschnitt",
@@ -278,6 +279,14 @@
"record.metadata.quality.updateFrequency.failed": "Aktualisierungsfrequenz nicht angegeben",
"record.metadata.quality.updateFrequency.success": "Aktualisierungsfrequenz angegeben",
"record.metadata.related": "Ähnliche Datensätze",
+ "record.metadata.userFeedbacks": "",
+ "record.metadata.userFeedbacks.anonymousUser": "",
+ "record.metadata.userFeedbacks.sortSelector.label": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "",
+ "record.metadata.userFeedbacks.newComment.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.buttonTitle": "",
"record.metadata.sheet": "Weitere Informationen verfügbar unter:",
"record.metadata.status": "Status",
"record.metadata.technical": "Technische Informationen",
@@ -351,6 +360,12 @@
"table.loading.data": "Daten werden geladen...",
"table.object.count": "Objekte in diesem Datensatz",
"table.select.data": "Datenquelle",
+ "timeSincePipe.lessThanAMinute": "",
+ "timeSincePipe.minutesAgo": "",
+ "timeSincePipe.hoursAgo": "",
+ "timeSincePipe.daysAgo": "",
+ "timeSincePipe.monthsAgo": "",
+ "timeSincePipe.yearsAgo": "",
"tooltip.html.copy": "HTML kopieren",
"tooltip.id.copy": "Eindeutige Kennung kopieren",
"tooltip.url.copy": "URL kopieren",
diff --git a/translations/en.json b/translations/en.json
index c633da1aec..64db73f843 100644
--- a/translations/en.json
+++ b/translations/en.json
@@ -1,4 +1,5 @@
{
+ "button.login": "Log in",
"catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}",
"catalog.figures.organisations": "{count, plural, =0{organisations} one{organisation} other{organisations}}",
"chart.aggregation.average": "average",
@@ -278,6 +279,14 @@
"record.metadata.quality.updateFrequency.failed": "Update frequency is not specified",
"record.metadata.quality.updateFrequency.success": "Update frequency is specified",
"record.metadata.related": "Related records",
+ "record.metadata.userFeedbacks": "Questions / Answers",
+ "record.metadata.userFeedbacks.anonymousUser": "In order to leave a comment, please log in.",
+ "record.metadata.userFeedbacks.sortSelector.label": "Sort by ...",
+ "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "Newest comments first",
+ "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "Oldest comments first",
+ "record.metadata.userFeedbacks.newComment.placeholder": "Write your comment here...",
+ "record.metadata.userFeedbacks.newAnswer.placeholder": "Answer...",
+ "record.metadata.userFeedbacks.newAnswer.buttonTitle": "Publish",
"record.metadata.sheet": "Original metadata",
"record.metadata.status": "Status",
"record.metadata.technical": "Technical information",
@@ -351,6 +360,12 @@
"table.loading.data": "Loading data...",
"table.object.count": "objects in this dataset",
"table.select.data": "Data source",
+ "timeSincePipe.lessThanAMinute": "Less than a minute ago",
+ "timeSincePipe.minutesAgo": "{value} minute{s} ago",
+ "timeSincePipe.hoursAgo": "{value} hour{s} ago",
+ "timeSincePipe.daysAgo": "{value} day{s} ago",
+ "timeSincePipe.monthsAgo": "{value} month{s} ago",
+ "timeSincePipe.yearsAgo": "{value} year{s} ago",
"tooltip.html.copy": "Copy HTML",
"tooltip.id.copy": "Copy unique identifier",
"tooltip.url.copy": "Copy URL",
diff --git a/translations/es.json b/translations/es.json
index d5fa70dbae..9eef1a70ac 100644
--- a/translations/es.json
+++ b/translations/es.json
@@ -1,4 +1,5 @@
{
+ "button.login": "",
"catalog.figures.datasets": "conjuntos de datos",
"catalog.figures.organisations": "organizaciones",
"chart.aggregation.average": "promedio",
@@ -278,6 +279,14 @@
"record.metadata.quality.updateFrequency.failed": "",
"record.metadata.quality.updateFrequency.success": "",
"record.metadata.related": "",
+ "record.metadata.userFeedbacks": "",
+ "record.metadata.userFeedbacks.anonymousUser": "",
+ "record.metadata.userFeedbacks.sortSelector.label": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "",
+ "record.metadata.userFeedbacks.newComment.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.buttonTitle": "",
"record.metadata.sheet": "",
"record.metadata.status": "",
"record.metadata.technical": "",
@@ -351,6 +360,12 @@
"table.loading.data": "",
"table.object.count": "",
"table.select.data": "",
+ "timeSincePipe.lessThanAMinute": "",
+ "timeSincePipe.minutesAgo": "",
+ "timeSincePipe.hoursAgo": "",
+ "timeSincePipe.daysAgo": "",
+ "timeSincePipe.monthsAgo": "",
+ "timeSincePipe.yearsAgo": "",
"tooltip.html.copy": "",
"tooltip.id.copy": "",
"tooltip.url.copy": "",
diff --git a/translations/fr.json b/translations/fr.json
index cb3231a31a..6f3ba2a211 100644
--- a/translations/fr.json
+++ b/translations/fr.json
@@ -1,4 +1,5 @@
{
+ "button.login": "Se connecter",
"catalog.figures.datasets": "{count, plural, =0{données} one{donnée} other{données}}",
"catalog.figures.organisations": "{count, plural, =0{organisations} one{organisation} other{organisations}}",
"chart.aggregation.average": "moyenne",
@@ -278,6 +279,14 @@
"record.metadata.quality.updateFrequency.failed": "Fréquence de mise à jour n'est pas renseignée",
"record.metadata.quality.updateFrequency.success": "Fréquence de mise à jour est renseignée",
"record.metadata.related": "Voir aussi",
+ "record.metadata.userFeedbacks": "Questions / Réponses",
+ "record.metadata.userFeedbacks.anonymousUser": "Pour rédiger un commentaire, veuillez vous identifier.",
+ "record.metadata.userFeedbacks.sortSelector.label": "Trier par ...",
+ "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "Les plus récents en premier",
+ "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "Les plus anciens en premier",
+ "record.metadata.userFeedbacks.newComment.placeholder": "Rédiger votre commentaire ici...",
+ "record.metadata.userFeedbacks.newAnswer.placeholder": "Répondre...",
+ "record.metadata.userFeedbacks.newAnswer.buttonTitle": "Publier",
"record.metadata.sheet": "Fiche de métadonnées d'origine",
"record.metadata.status": "Statut",
"record.metadata.technical": "Informations techniques",
@@ -351,6 +360,12 @@
"table.loading.data": "Chargement des données...",
"table.object.count": "enregistrements dans ces données",
"table.select.data": "Source de données",
+ "timeSincePipe.lessThanAMinute": "Il y a moins d'une minute",
+ "timeSincePipe.minutesAgo": "Il y a {value} minute{s}",
+ "timeSincePipe.hoursAgo": "Il y a {value} heure{s}",
+ "timeSincePipe.daysAgo": "Il y a {value} jour{s}",
+ "timeSincePipe.monthsAgo": "Il y a {value} mois",
+ "timeSincePipe.yearsAgo": "Il y a {value} an{s}",
"tooltip.html.copy": "Copier le HTML",
"tooltip.id.copy": "Copier l'identifiant unique",
"tooltip.url.copy": "Copier l'URL",
diff --git a/translations/it.json b/translations/it.json
index ffee1b9e90..71cdd0bd42 100644
--- a/translations/it.json
+++ b/translations/it.json
@@ -1,4 +1,5 @@
{
+ "button.login": "",
"catalog.figures.datasets": "{count, plural, =0{datasets} one{dataset} other{datasets}}",
"catalog.figures.organisations": "{count, plural, =0{organizzazioni} one{organizzazione} other{organizzazioni}}",
"chart.aggregation.average": "media",
@@ -278,6 +279,14 @@
"record.metadata.quality.updateFrequency.failed": "La frequenza di aggiornamento non è specificata",
"record.metadata.quality.updateFrequency.success": "La frequenza di aggiornamento è specificata",
"record.metadata.related": "Vedi anche",
+ "record.metadata.userFeedbacks": "",
+ "record.metadata.userFeedbacks.anonymousUser": "",
+ "record.metadata.userFeedbacks.sortSelector.label": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "",
+ "record.metadata.userFeedbacks.newComment.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.buttonTitle": "",
"record.metadata.sheet": "Origine del metadata",
"record.metadata.status": "Stato",
"record.metadata.technical": "Informazioni tecniche",
@@ -351,6 +360,12 @@
"table.loading.data": "Caricamento dei dati...",
"table.object.count": "record in questi dati",
"table.select.data": "Sorgente dati",
+ "timeSincePipe.lessThanAMinute": "",
+ "timeSincePipe.minutesAgo": "",
+ "timeSincePipe.hoursAgo": "",
+ "timeSincePipe.daysAgo": "",
+ "timeSincePipe.monthsAgo": "",
+ "timeSincePipe.yearsAgo": "",
"tooltip.html.copy": "Copiare il HTML",
"tooltip.id.copy": "Copiare l'identificatore unico",
"tooltip.url.copy": "Copiare l'URL",
diff --git a/translations/nl.json b/translations/nl.json
index 0e44dc0b00..d29ab911e8 100644
--- a/translations/nl.json
+++ b/translations/nl.json
@@ -1,4 +1,5 @@
{
+ "button.login": "",
"catalog.figures.datasets": "datasets",
"catalog.figures.organisations": "organisaties",
"chart.aggregation.average": "gemiddelde",
@@ -278,6 +279,14 @@
"record.metadata.quality.updateFrequency.failed": "",
"record.metadata.quality.updateFrequency.success": "",
"record.metadata.related": "",
+ "record.metadata.userFeedbacks": "",
+ "record.metadata.userFeedbacks.anonymousUser": "",
+ "record.metadata.userFeedbacks.sortSelector.label": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "",
+ "record.metadata.userFeedbacks.newComment.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.buttonTitle": "",
"record.metadata.sheet": "",
"record.metadata.status": "",
"record.metadata.technical": "",
@@ -351,6 +360,12 @@
"table.loading.data": "",
"table.object.count": "",
"table.select.data": "",
+ "timeSincePipe.lessThanAMinute": "",
+ "timeSincePipe.minutesAgo": "",
+ "timeSincePipe.hoursAgo": "",
+ "timeSincePipe.daysAgo": "",
+ "timeSincePipe.monthsAgo": "",
+ "timeSincePipe.yearsAgo": "",
"tooltip.html.copy": "",
"tooltip.id.copy": "",
"tooltip.url.copy": "",
diff --git a/translations/pt.json b/translations/pt.json
index 6898e80589..7acbb1d8f1 100644
--- a/translations/pt.json
+++ b/translations/pt.json
@@ -1,4 +1,5 @@
{
+ "button.login": "",
"catalog.figures.datasets": "conjuntos de dados",
"catalog.figures.organisations": "organizações",
"chart.aggregation.average": "média",
@@ -278,6 +279,14 @@
"record.metadata.quality.updateFrequency.failed": "",
"record.metadata.quality.updateFrequency.success": "",
"record.metadata.related": "",
+ "record.metadata.userFeedbacks": "",
+ "record.metadata.userFeedbacks.anonymousUser": "",
+ "record.metadata.userFeedbacks.sortSelector.label": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "",
+ "record.metadata.userFeedbacks.newComment.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.buttonTitle": "",
"record.metadata.sheet": "",
"record.metadata.status": "",
"record.metadata.technical": "",
@@ -351,6 +360,12 @@
"table.loading.data": "",
"table.object.count": "",
"table.select.data": "",
+ "timeSincePipe.lessThanAMinute": "",
+ "timeSincePipe.minutesAgo": "",
+ "timeSincePipe.hoursAgo": "",
+ "timeSincePipe.daysAgo": "",
+ "timeSincePipe.monthsAgo": "",
+ "timeSincePipe.yearsAgo": "",
"tooltip.html.copy": "",
"tooltip.id.copy": "",
"tooltip.url.copy": "",
diff --git a/translations/sk.json b/translations/sk.json
index 35dc635932..9a6bf1cc0d 100644
--- a/translations/sk.json
+++ b/translations/sk.json
@@ -1,4 +1,5 @@
{
+ "button.login": "",
"catalog.figures.datasets": "{count, plural, =0{datasety} one{dataset} other{datasety}}",
"catalog.figures.organisations": "{count, plural, =0{organizácie} one{organizácia} other{organizácie}}",
"chart.aggregation.average": "priemer",
@@ -278,6 +279,14 @@
"record.metadata.quality.updateFrequency.failed": "Frekvencia aktualizácie nie je určená",
"record.metadata.quality.updateFrequency.success": "Frekvencia aktualizácie je určená",
"record.metadata.related": "Súvisiace záznamy",
+ "record.metadata.userFeedbacks": "",
+ "record.metadata.userFeedbacks.anonymousUser": "",
+ "record.metadata.userFeedbacks.sortSelector.label": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "",
+ "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "",
+ "record.metadata.userFeedbacks.newComment.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.placeholder": "",
+ "record.metadata.userFeedbacks.newAnswer.buttonTitle": "",
"record.metadata.sheet": "Ďalšie metadáta sú k dispozícii na:",
"record.metadata.status": "Stav",
"record.metadata.technical": "",
@@ -351,6 +360,12 @@
"table.loading.data": "Načítanie údajov...",
"table.object.count": "objekty v tomto súbore údajov",
"table.select.data": "Zdroj údajov",
+ "timeSincePipe.lessThanAMinute": "",
+ "timeSincePipe.minutesAgo": "",
+ "timeSincePipe.hoursAgo": "",
+ "timeSincePipe.daysAgo": "",
+ "timeSincePipe.monthsAgo": "",
+ "timeSincePipe.yearsAgo": "",
"tooltip.html.copy": "Kopírovať HTML",
"tooltip.id.copy": "Kopírovať jedinečný identifikátor",
"tooltip.url.copy": "Kopírovať URL",