diff --git a/apps/datahub/src/app/app.module.ts b/apps/datahub/src/app/app.module.ts index 9c4b458339..01ba81a605 100644 --- a/apps/datahub/src/app/app.module.ts +++ b/apps/datahub/src/app/app.module.ts @@ -80,6 +80,7 @@ import { RecordOtherlinksComponent } from './record/record-otherlinks/record-oth import { RecordDownloadsComponent } from './record/record-downloads/record-downloads.component' import { RecordApisComponent } from './record/record-apis/record-apis.component' import { MatTabsModule } from '@angular/material/tabs' +import { RecordUserFeedbacksComponent } from './record/record-user-feedbacks/record-user-feedbacks.component' export const metaReducers: MetaReducer[] = !environment.production ? [] : [] // https://github.com/nrwl/nx/issues/191 @@ -100,6 +101,7 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] KeyFiguresComponent, NavigationMenuComponent, RecordRelatedRecordsComponent, + RecordUserFeedbacksComponent, RecordMetadataComponent, RecordOtherlinksComponent, RecordDownloadsComponent, diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html index 51ecee9de2..e8df838b41 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.html +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.html @@ -77,7 +77,7 @@ record.tab.maprecord.tab.map
+
+
+ +
+
mapLinks?.length > 0 || geoDataLinks?.length > 0 ) ) + displayData$ = combineLatest([ this.facade.dataLinks$, - this.facade.geoDataLinks$, + this.facade.geoDataLinks$ ]).pipe( map( ([dataLinks, geoDataLinks]) => dataLinks?.length > 0 || geoDataLinks?.length > 0 ) ) + displayDownload$ = this.facade.downloadLinks$.pipe( map((links) => links?.length > 0) ) displayApi$ = this.facade.apiLinks$.pipe(map((links) => links?.length > 0)) + displayOtherLinks = this.facade.otherLinks$.pipe( map((links) => links?.length > 0) ) @@ -49,6 +52,11 @@ export class RecordMetadataComponent { map((records) => records?.length > 0) ) + organisationName$ = this.facade.metadata$.pipe( + map((record) => record?.ownerOrganization?.name), + filter(Boolean) + ) + sourceLabel$ = this.facade.metadata$.pipe( map((record) => record?.extras?.catalogUuid as string), filter((uuid) => !!uuid), @@ -78,7 +86,8 @@ export class RecordMetadataComponent { private searchService: SearchService, private sourceService: SourcesService, private orgsService: OrganizationsServiceInterface - ) {} + ) { + } onTabIndexChange(index: number): void { this.selectedTabIndex$.next(index) @@ -90,6 +99,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..261f4cbfed --- /dev/null +++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.html @@ -0,0 +1,50 @@ +

+ record.metadata.userFeedbacks +

+
+ +
+ + + Publish + + +
+
+
+
+ +
+
+ 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..64ddce2625 --- /dev/null +++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { RecordUserFeedbacksComponent } from './record-user-feedbacks.component' + +describe('RelatedRecordsComponent', () => { + let component: RecordUserFeedbacksComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RecordUserFeedbacksComponent], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(RecordUserFeedbacksComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) 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..42ada6714f --- /dev/null +++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.component.ts @@ -0,0 +1,109 @@ +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core' +import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { filter, takeUntil } from 'rxjs/operators' +import { RecordUserFeedbacksService } from './record-user-feedbacks.service' +import { Observable, Subject } from 'rxjs' +import { UserFeedback } 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' + +@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 + + private destroy$ = new Subject() + + userFeedbacksParents: UserFeedback[] = [] + userFeedBacksAnswers: Map = new Map() + + isActiveUserEditor = false + newComment = '' + + isNewCommentEmpty = true + + activeUser?: UserModel + + constructor( + private facade: MdViewFacade, + private userFeedBackService: RecordUserFeedbacksService, + private cdr: ChangeDetectorRef, + private platformServiceInterface: PlatformServiceInterface + ) { + } + + ngOnInit(): void { + + this.platformServiceInterface.getMe() + .pipe( + filter(Boolean), + takeUntil(this.destroy$) + ).subscribe(user => { + this.activeUser = user + this.isActiveUserEditor = + user.profile === 'Administrator' + || user.profile === 'UserAdmin' + || user.profile === 'Reviewer' + || user.profile === 'Editor' + }) + + this.facade.userFeedbacks$.pipe( + filter(Boolean), + takeUntil(this.destroy$) + ).subscribe(userFeedbacks => { + + this.userFeedbacksParents = userFeedbacks + .filter(userFeedbacks => userFeedbacks.parentUuid === null) + .sort(this.userFeedBackService.sortByDateFromNewestToOldest) + + const enfants = userFeedbacks + .filter(userFeedbacks => userFeedbacks.parentUuid !== null) + .sort(this.userFeedBackService.sortByDateFromOldestToNewest) + + enfants.forEach(userFeedbacksAnswer => { + const parent = this.userFeedBacksAnswers.get(userFeedbacksAnswer.parentUuid) + if (parent) { + parent.push(userFeedbacksAnswer) + } else { + this.userFeedBacksAnswers.set(userFeedbacksAnswer.parentUuid, [userFeedbacksAnswer]) + } + }) + + this.cdr.markForCheck() + }) + } + + ngOnDestroy(): void { + this.destroy$.next() + this.destroy$.complete() + } + + onNewCommentValueChange() { + this.isNewCommentEmpty = this.newComment.length === 0 + } + + onNewAnswer(newUserFeedback: UserFeedback) { + this.userFeedBackService.postUserFeedback(newUserFeedback) + } + + publishNewComment() { + const newFeedback: UserFeedback = { + uuid: undefined, + comment: this.newComment, + parentUuid: null, + date: undefined, + authorUserId: Number.parseInt(this.activeUser?.id), // todo: il faut faire quelque chose avec ces userId number alors que tout le reste est en uuid... + authorEmail: this.activeUser?.email, + authorName: `${this.activeUser?.name} ${this.activeUser?.surname}` + } + + this.userFeedBackService.postUserFeedback(newFeedback) + this.newComment = '' + } +} diff --git a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.service.spec.ts b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.service.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.service.ts b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.service.ts new file mode 100644 index 0000000000..720b865ccf --- /dev/null +++ b/apps/datahub/src/app/record/record-user-feedbacks/record-user-feedbacks.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core' +import { UserFeedback } from '@geonetwork-ui/common/domain/model/record' +import { PlatformServiceInterface } from '@geonetwork-ui/common/domain/platform.service.interface' + +@Injectable({ + providedIn: 'root', +}) +export class RecordUserFeedbacksService { + + constructor( + private platformServiceInterface: PlatformServiceInterface + ) { + } + + postUserFeedback(userFeedback: UserFeedback): void { + this.platformServiceInterface.postUserFeedbacks(userFeedback) + } + + sortByDateFromNewestToOldest(userFeedbackA: UserFeedback, userFeedbackB: UserFeedback): number { + return new Date(userFeedbackB.date).getTime() - new Date(userFeedbackA.date).getTime() + } + + sortByDateFromOldestToNewest(userFeedbackA: UserFeedback, userFeedbackB: UserFeedback): number { + return new Date(userFeedbackA.date).getTime() - new Date(userFeedbackB.date).getTime() + } +} 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..4aaf4ccb48 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 @@ -8,10 +8,13 @@ import { AvatarServiceInterface } from '../auth/avatar.service.interface' import { map } from 'rxjs/operators' import { Observable, of } from 'rxjs' import { ThesaurusModel } from '@geonetwork-ui/common/domain/model/thesaurus/thesaurus.model' +import { UserFeedback } from '@geonetwork-ui/common/domain/model/record' @Injectable() export class Gn4PlatformMapper { - constructor(private avatarService: AvatarServiceInterface) {} + constructor(private avatarService: AvatarServiceInterface) { + } + userFromMeApi(apiUser: MeResponseApiModel): Observable { if (!apiUser) return of(null) const { @@ -28,6 +31,7 @@ export class Gn4PlatformMapper { .getProfileIcon(hash) .pipe(map((profileIcon) => ({ ...user, profileIcon } as UserModel))) } + userFromApi(apiUser: UserApiModel): UserModel { if (!apiUser) return null const { @@ -65,4 +69,17 @@ export class Gn4PlatformMapper { } }) } + + userFeedbacksFromApi(userFeedbacks: any[]): UserFeedback[] { + return userFeedbacks.map(userFeedback => ({ + uuid: userFeedback.uuid, + comment: userFeedback.comment, + authorUserId: userFeedback.authorUserId, + authorName: userFeedback.authorName, + authorEmail: userFeedback.authorEmail, + published: userFeedback.published, + parentUuid: userFeedback.parentUuid, + date: userFeedback.date, + })) + } } 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..443803ea63 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 @@ -5,12 +5,12 @@ import { MeApiService, RegistriesApiService, SiteApiService, - ToolsApiService, + 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' @@ -65,7 +65,8 @@ 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)), @@ -150,4 +151,22 @@ export class Gn4PlatformService implements PlatformServiceInterface { ) return this.thesauri[uri] } + + getUserFeedbacks(uuid: string): Observable { + return this.userfeedbackApiService.getUserComments(uuid) + .pipe( + map((userFeedbacks) => + this.mapper.userFeedbacksFromApi( + userFeedbacks + ) + ), + shareReplay(1) + ) + } + + postUserFeedbacks(userFeedback: UserFeedback): void{ + this.userfeedbackApiService.newUserFeedback(userFeedback).subscribe(test => { + console.log(test) + }) + } } 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..e5ac4f287e --- /dev/null +++ b/libs/common/domain/src/lib/model/record/user-feedbacks.model.ts @@ -0,0 +1,10 @@ +export interface UserFeedback { + uuid: string + comment: string + authorUserId: number + authorName: string + authorEmail: string + published: boolean + parentUuid?: string + date: 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..3d1e01bcb5 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): void } diff --git a/libs/feature/record/src/lib/state/mdview.actions.ts b/libs/feature/record/src/lib/state/mdview.actions.ts index 9b2172888c..4770ad4b5f 100644 --- a/libs/feature/record/src/lib/state/mdview.actions.ts +++ b/libs/feature/record/src/lib/state/mdview.actions.ts @@ -1,6 +1,6 @@ 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' export const loadFullMetadata = createAction( '[Metadata view] Load full metadata', @@ -27,6 +27,11 @@ export const setRelated = createAction( props<{ related: CatalogRecord[] }>() ) +export const setUserFeedbacks = createAction( + '[Metadata view] Set user feedback', + props<{ userFeedbacks: UserFeedback[] }>() +) + export const setChartConfig = createAction( '[Metadata view] Set chart config', props<{ chartConfig: DatavizConfigurationModel }>() diff --git a/libs/feature/record/src/lib/state/mdview.effects.ts b/libs/feature/record/src/lib/state/mdview.effects.ts index 66893f90f4..4c546c5a1e 100644 --- a/libs/feature/record/src/lib/state/mdview.effects.ts +++ b/libs/feature/record/src/lib/state/mdview.effects.ts @@ -4,12 +4,14 @@ import { 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(() => @@ -40,4 +42,19 @@ export class MdViewEffects { catchError((error) => of(MdViewActions.setRelated({ related: null }))) ) ) + + loadUserFeedbacks$ = createEffect(() => + this.actions$.pipe( + ofType(MdViewActions.loadFullMetadata), + switchMap(({ uuid }) => + this.platformServiceInterface.getUserFeedbacks(uuid) + ), + map((userFeedbacks) => { + return MdViewActions.setUserFeedbacks({ userFeedbacks }) + }), + catchError((error) => + of(MdViewActions.loadFullFailure({ otherError: error.message })) + ) + ) + ) } diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index a2e4a0064f..97f35a6f3c 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -6,6 +6,7 @@ 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 { getUserFeedbacks } from './mdview.selectors' @Injectable() /** @@ -34,6 +35,8 @@ export class MdViewFacade { chartConfig$ = this.store.pipe(select(MdViewSelectors.getChartConfig)) + userFeedbacks$ = this.store.pipe(select(MdViewSelectors.getUserFeedbacks)) + allLinks$ = this.metadata$.pipe( map((record) => ('distributions' in record ? record.distributions : [])) ) diff --git a/libs/feature/record/src/lib/state/mdview.reducer.ts b/libs/feature/record/src/lib/state/mdview.reducer.ts index 81b6ba7e08..b50b9d15f7 100644 --- a/libs/feature/record/src/lib/state/mdview.reducer.ts +++ b/libs/feature/record/src/lib/state/mdview.reducer.ts @@ -1,7 +1,7 @@ import { Action, createReducer, on } from '@ngrx/store' import * as MdViewActions 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' @@ -10,12 +10,13 @@ export interface MdViewState { error: { notFound?: boolean; otherError?: string } | null metadata?: Partial related?: CatalogRecord[] + userFeedbacks?: UserFeedback[] chartConfig?: DatavizConfigurationModel } export const initialMdviewState: MdViewState = { error: null, - loadingFull: false, + loadingFull: false } const mdViewReducer = createReducer( @@ -45,13 +46,17 @@ const mdViewReducer = createReducer( ...state, related, })), + on(MdViewActions.setUserFeedbacks, (state, { userFeedbacks }) => ({ + ...state, + userFeedbacks, + })), on(MdViewActions.setChartConfig, (state, { chartConfig }) => ({ ...state, chartConfig, })), on(MdViewActions.close, (state) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { metadata, related, ...stateWithoutMd } = state + const { metadata, related, userFeedbacks, ...stateWithoutMd } = state return stateWithoutMd }) ) diff --git a/libs/feature/record/src/lib/state/mdview.selectors.ts b/libs/feature/record/src/lib/state/mdview.selectors.ts index ee79e613be..445c0503fa 100644 --- a/libs/feature/record/src/lib/state/mdview.selectors.ts +++ b/libs/feature/record/src/lib/state/mdview.selectors.ts @@ -30,6 +30,10 @@ export const getRelated = createSelector( getMdViewState, (state: MdViewState) => state.related ) +export const getUserFeedbacks = createSelector( + getMdViewState, + (state: MdViewState) => state.userFeedbacks +) export const getChartConfig = createSelector( getMdViewState, (state: MdViewState) => state.chartConfig diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index 7a6feab03f..d9c4f0b7f6 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 { DayCountPipePipe } from './user-feedback-item/day-count.pipe' @NgModule({ imports: [ @@ -46,6 +48,7 @@ import { ImageOverlayPreviewComponent } from './image-overlay-preview/image-over FormsModule, NgOptimizedImage, MarkdownParserComponent, + DayCountPipePipe, ], 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/day-count.pipe.ts b/libs/ui/elements/src/lib/user-feedback-item/day-count.pipe.ts new file mode 100644 index 0000000000..aa803e450e --- /dev/null +++ b/libs/ui/elements/src/lib/user-feedback-item/day-count.pipe.ts @@ -0,0 +1,37 @@ +import { Pipe, PipeTransform } from '@angular/core' + +@Pipe({ + name: 'dayCount', + standalone: true, +}) +export class DayCountPipePipe implements PipeTransform { + + transform(value: Date | string): string { + let givenDate: Date + + if (typeof value === 'string') { + givenDate = new Date(value) + if (isNaN(givenDate.getTime())) { + return 'Date invalide' + } + } else { + givenDate = value + } + + const today = new Date() + today.setHours(0, 0, 0, 0) + givenDate.setHours(0, 0, 0, 0) + + const timeDiff = today.getTime() - givenDate.getTime() + const dayTimeDiff = Math.round(timeDiff / (1000 * 3600 * 24)) + + if (dayTimeDiff === 0) { + return 'Aujourd\'hui' // TODO <--------------------------------- TODO: Translate + } else if (dayTimeDiff === 1) { + return 'Hier' + } else { + return `Il y a ${dayTimeDiff} jours` + } + } + +} 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..129754ed7a --- /dev/null +++ b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.html @@ -0,0 +1,62 @@ +
+
+
+ +
+
+ {{ userFeedback.authorName }} + {{ userFeedback.date | dayCount }} +
+
+
+ {{ userFeedback.comment }} +
+
+
+ +
+ +
+
+ +
+ + + Publish + + +
+
+
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..1abc7bf70b --- /dev/null +++ b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.spec.ts @@ -0,0 +1,102 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MatIconModule } from '@angular/material/icon' +import { TranslateModule } from '@ngx-translate/core' +import { UserFeedbackItemComponent } from './user-feedback-item.component' +import { By } from '@angular/platform-browser' +import { ChangeDetectionStrategy, DebugElement } from '@angular/core' + +describe('DownloadsListItemComponent', () => { + let component: UserFeedbackItemComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [UserFeedbackItemComponent], + imports: [MatIconModule, TranslateModule.forRoot()], + }) + .overrideComponent(UserFeedbackItemComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(UserFeedbackItemComponent) + component = fixture.componentInstance + component.link = { + name: 'allroads.geojson', + description: 'A file that contains all roads', + url: new URL('https://roads.com/allroads.geojson'), + type: 'download', + } + component.format = 'geojson' + component.color = 'red' + + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + describe('download description', () => { + describe('when has a description', () => { + it('displays the description', () => { + const descElmt = fixture.debugElement.query(By.css('.text-21')) + + expect(descElmt.attributes.title).toEqual( + 'A file that contains all roads' + ) + expect(descElmt.nativeElement.textContent.trim()).toEqual( + 'A file that contains all roads' + ) + }) + }) + describe('when has no description', () => { + beforeEach(() => { + delete component.link.description + fixture.detectChanges() + }) + it('displays name', () => { + const descElmt = fixture.debugElement.query(By.css('.text-21')) + + expect(descElmt.attributes.title).toEqual('allroads.geojson') + expect(descElmt.nativeElement.textContent.trim()).toEqual( + 'allroads.geojson' + ) + }) + }) + }) + describe('download format', () => { + let badgeElt: DebugElement + let spans: DebugElement[] + beforeEach(() => { + spans = fixture.debugElement.queryAll(By.css('span')) + badgeElt = spans[0] + }) + it('displays the format in the badge', () => { + expect(badgeElt.nativeElement.textContent.trim()).toEqual('geojson') + }) + it('set the badge color', () => { + expect(badgeElt.styles['background-color']).toEqual('red') + }) + + describe('when it is not Wfs', () => { + it('do not display wfs information', () => { + const spans = fixture.debugElement.queryAll(By.css('span')) + + expect(spans.length).toBe(1) + }) + }) + describe('when it is not Wfs', () => { + beforeEach(() => { + component.isFromWfs = true + fixture.detectChanges() + }) + it('add wfs information', () => { + const spans = fixture.debugElement.queryAll(By.css('span')) + + expect(spans.length).toBe(2) + }) + }) + }) +}) 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..0caa7c9127 --- /dev/null +++ b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.stories.ts @@ -0,0 +1,45 @@ +import { + applicationConfig, + componentWrapperDecorator, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { UserFeedbackItemComponent } from './user-feedback-item.component' +import { BrowserAnimationsModule } from '@angular/platform-browser/animations' +import { TranslateModule } from '@ngx-translate/core' +import { importProvidersFrom } from '@angular/core' +import { MatIcon } from '@angular/material/icon' + +export default { + title: 'Elements/DownloadsListItemComponent', + component: UserFeedbackItemComponent, + decorators: [ + moduleMetadata({ + declarations: [MatIcon], + imports: [TranslateModule.forRoot()], + }), + applicationConfig({ + providers: [importProvidersFrom(BrowserAnimationsModule)], + }), + componentWrapperDecorator( + (story) => `
${story}
` + ), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + link: { + name: 'allroads.geojson', + type: 'download', + description: 'A file that contains all roads', + url: new URL('https://roads.com/allroads.geojson'), + }, + }, + argTypes: { + exportUrl: { + action: 'exportUrl', + }, + }, +} 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..061b1df864 --- /dev/null +++ b/libs/ui/elements/src/lib/user-feedback-item/user-feedback-item.component.ts @@ -0,0 +1,60 @@ +import { + Component, + ChangeDetectionStrategy, + Input, OnInit, Output, EventEmitter +} from '@angular/core' +import { UserFeedback } from '@geonetwork-ui/common/domain/model/record' +import { GravatarService } from '@geonetwork-ui/api/repository' +import { Observable } from 'rxjs' +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() userFeedback: UserFeedback + @Input() userFeedBacksAnswers: UserFeedback[] + @Input() isActiveUserEditor: boolean + @Input() activeUser: UserModel + @Input() isFirstComment: boolean + + isAnAnswer = false + + avatarUrl$: Observable + + newAnswer = '' + isAnswerEmpty = true + + @Output() newAnswerEmitter = new EventEmitter() + + constructor( + private gravatarService: GravatarService + ) { + } + + ngOnInit(): void { + this.isAnAnswer = this.userFeedback.parentUuid !== null + this.avatarUrl$ = this.gravatarService.getProfileIcon(this.userFeedback.authorUserId?.toString()) + } + + onNewAnswerValueChange() { + this.isAnswerEmpty = this.newAnswer.length === 0 + } + + publishNewAnswer() { + this.newAnswerEmitter.emit({ + ...this.userFeedback, + uuid: undefined, + comment: this.newAnswer, + parentUuid: this.userFeedback.uuid, + authorUserId: Number.parseInt(this.activeUser?.id), // todo: Shouldn't we do something with this? Convert it to uuid like it's done everywhere else ?... + authorEmail: this.activeUser?.email, + authorName: `${this.activeUser?.name} ${this.activeUser?.surname}` + }) + this.newAnswer = '' + } +} 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 + constructor() { + this.baseClass = 'appearance-none border border-gray-300 rounded w-full p-2 text-gray-700 leading-tight focus:outline-none focus:border-primary' + } + + get classList() { + return `${this.baseClass} ${this.extraClass}` + } + ngAfterViewInit() { this.checkRequired(this.input.nativeElement.value) } diff --git a/support-services/.env b/support-services/.env index 4ee8267eca..a5b62dae2d 100644 --- a/support-services/.env +++ b/support-services/.env @@ -1 +1 @@ -GEONETWORK_VERSION=4.2.2 +GEONETWORK_VERSION=4.2.8 diff --git a/support-services/docker-compose.yml b/support-services/docker-compose.yml index 8ee3b01ff6..bb0b112777 100644 --- a/support-services/docker-compose.yml +++ b/support-services/docker-compose.yml @@ -11,6 +11,8 @@ services: POSTGRES_USER: geonetwork POSTGRES_PASSWORD: geonetwork POSTGRES_DB: geonetwork + ports: + - 5432:5432 healthcheck: test: ['CMD-SHELL', 'pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB'] interval: 10s diff --git a/translations/de.json b/translations/de.json index f8324d3d2e..236b1e613d 100644 --- a/translations/de.json +++ b/translations/de.json @@ -258,6 +258,7 @@ "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.sheet": "Weitere Informationen verfügbar unter:", "record.metadata.status": "Status", "record.metadata.technical": "Technische Informationen", diff --git a/translations/en.json b/translations/en.json index dd45df888c..8aa047032b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -258,6 +258,7 @@ "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 / Answer", "record.metadata.sheet": "Original metadata", "record.metadata.status": "Status", "record.metadata.technical": "Technical information", diff --git a/translations/es.json b/translations/es.json index 9f82e0ab23..19d6d8f503 100644 --- a/translations/es.json +++ b/translations/es.json @@ -258,6 +258,7 @@ "record.metadata.quality.updateFrequency.failed": "", "record.metadata.quality.updateFrequency.success": "", "record.metadata.related": "", + "record.metadata.userFeedbacks": "", "record.metadata.sheet": "", "record.metadata.status": "", "record.metadata.technical": "", diff --git a/translations/fr.json b/translations/fr.json index 799605a027..26a3eba521 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -258,6 +258,7 @@ "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.sheet": "Fiche de métadonnées d'origine", "record.metadata.status": "Statut", "record.metadata.technical": "Informations techniques", diff --git a/translations/it.json b/translations/it.json index 9b03fabaca..ffb780bb0c 100644 --- a/translations/it.json +++ b/translations/it.json @@ -258,6 +258,7 @@ "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.sheet": "Origine del metadata", "record.metadata.status": "Stato", "record.metadata.technical": "Informazioni tecniche", diff --git a/translations/nl.json b/translations/nl.json index 64ee837518..71582bff23 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -258,6 +258,7 @@ "record.metadata.quality.updateFrequency.failed": "", "record.metadata.quality.updateFrequency.success": "", "record.metadata.related": "", + "record.metadata.userFeedbacks": "", "record.metadata.sheet": "", "record.metadata.status": "", "record.metadata.technical": "", diff --git a/translations/pt.json b/translations/pt.json index 86fe64024b..4f01b69bc1 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -258,6 +258,7 @@ "record.metadata.quality.updateFrequency.failed": "", "record.metadata.quality.updateFrequency.success": "", "record.metadata.related": "", + "record.metadata.userFeedbacks": "", "record.metadata.sheet": "", "record.metadata.status": "", "record.metadata.technical": "", diff --git a/translations/sk.json b/translations/sk.json index db63a71d01..01dcbebbba 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -258,6 +258,7 @@ "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.sheet": "Ďalšie metadáta sú k dispozícii na:", "record.metadata.status": "Stav", "record.metadata.technical": "",