From ad0c7635b302d919328970832d445c324c23c91a Mon Sep 17 00:00:00 2001 From: Romuald Caplier <romuald.caplier@camptocamp.com> Date: Fri, 24 May 2024 15:34:11 +0200 Subject: [PATCH] feat(datahub): finish organization-page --- .../src/e2e/organization-page.cy.ts | 125 ++++++++++++++++++ apps/datahub-e2e/src/e2e/organizations.cy.ts | 9 +- .../organization-details.component.css | 15 +++ .../organization-details.component.html | 24 +++- .../organization-details.component.spec.ts | 4 - .../organization-details.component.ts | 53 +++++--- .../organization-header.component.css | 0 .../organization-header.component.html | 2 + .../organization-header.component.spec.ts | 0 .../organization-header.component.ts | 0 .../organization-page.component.ts | 2 +- .../domain/src/lib/model/user/user.model.ts | 2 +- .../catalog/src/lib/my-org/my-org.service.ts | 4 +- .../related-record-card.component.html | 2 +- .../related-record-card.component.ts | 23 +++- .../docker-entrypoint-initdb.d/dump | Bin 460488 -> 460652 bytes 16 files changed, 234 insertions(+), 31 deletions(-) create mode 100644 apps/datahub-e2e/src/e2e/organization-page.cy.ts rename apps/datahub/src/app/organization/{header-organization => organization-header}/organization-header.component.css (100%) rename apps/datahub/src/app/organization/{header-organization => organization-header}/organization-header.component.html (95%) rename apps/datahub/src/app/organization/{header-organization => organization-header}/organization-header.component.spec.ts (100%) rename apps/datahub/src/app/organization/{header-organization => organization-header}/organization-header.component.ts (100%) diff --git a/apps/datahub-e2e/src/e2e/organization-page.cy.ts b/apps/datahub-e2e/src/e2e/organization-page.cy.ts new file mode 100644 index 0000000000..af3b7783a1 --- /dev/null +++ b/apps/datahub-e2e/src/e2e/organization-page.cy.ts @@ -0,0 +1,125 @@ +import 'cypress-real-events' + +describe('organizations', () => { + beforeEach(() => { + cy.visit('organization/Barbie%20Inc.') + + // aliases + cy.get('gn-ui-navigation-button').as('backButton') + cy.get('[data-cy="organizationHeaderName"]').as('organizationHeaderName') + cy.get('[data-cy="organizationHeaderWebsiteLink"]').as( + 'organizationHeaderWebsiteLink' + ) + cy.get('[data-cy="organizationDescription"]').as('organizationDescription') + cy.get('gn-ui-max-lines').contains('Read more').as('readMoreButton') + cy.get('[data-cy="organizationLogo"]').as('organizationLogo') + cy.get('[data-cy="organizationDatasetCount"]').as( + 'organizationDatasetCount' + ) + cy.get('[data-cy="organizationEmail"]').as('organizationEmail') + cy.get('[data-cy="organizationPageLastPublishedDatasets"]').as( + 'organizationPageLastPublishedDatasets' + ) + cy.get( + '[data-cy="organizationDetailsLastPublishedDatasetsSearchAllButton"]' + ).as('organizationDetailsLastPublishedDatasetsSearchAllButton') + }) + + describe('general display', () => { + describe('header', () => { + describe('back button', () => { + beforeEach(() => { + // Simulate that we come from the organizations search page + cy.visit('organisations') + cy.visit('organization/Barbie%20Inc.') + }) + + it('back button goes to the previous visited page', () => { + cy.get('@backButton').click() + cy.url().should('include', '/organisations') + }) + }) + + it('should display the organization name', () => { + cy.get('@organizationHeaderName').should('contain', 'Barbie Inc.') + }) + + it('should display the organization website link', () => { + cy.get('@organizationHeaderWebsiteLink') + .should('be.visible') + .should('have.attr', 'href', 'https://www.barbie-inc.com/') + .and('have.attr', 'target', '_blank') + }) + }) + + describe('details', () => { + describe('left column', () => { + it('should display the organization description', () => { + cy.get('@organizationDescription').should('be.visible') + }) + + it('click on read more should expand the organization description', () => { + let initialDescription + let newDescription + + cy.get('@organizationDescription').then((firstDescription) => { + initialDescription = firstDescription + cy.get('@readMoreButton').trigger('click') + cy.get('@organizationDescription').then((secondDescription) => { + newDescription = secondDescription + expect(newDescription).to.not.equal(initialDescription) + }) + }) + }) + }) + + describe('right column', () => { + it('should display the organization logo', () => { + cy.get('@organizationLogo').should('be.visible') + }) + + it('should display the organization dataset count', () => { + cy.get('@organizationDatasetCount').should('be.visible') + }) + + it('a click on the organization dataset count should open the dataset search page filtered on the organization', () => { + cy.get('@organizationDatasetCount').then(($link) => { + const url = $link.prop('href') + cy.wrap($link).click() + + cy.url().should('eq', url) + }) + }) + + it('should display the organization email', () => { + cy.get('@organizationEmail') + .should('be.visible') + .and('have.attr', 'href', 'mailto:contact@barbie-inc.com') + }) + }) + + describe('last published datasets', () => { + it('should display the last published datasets', () => { + cy.get('@organizationPageLastPublishedDatasets').should('be.visible') + }) + + it('should display the search all button', () => { + cy.get( + '@organizationDetailsLastPublishedDatasetsSearchAllButton' + ).should('be.visible') + }) + + it('a click on the search all button should open the dataset search page filtered on the organization', () => { + cy.get( + '@organizationDetailsLastPublishedDatasetsSearchAllButton' + ).then(($link) => { + const url = $link.prop('href') + cy.wrap($link).click() + + cy.url().should('eq', url) + }) + }) + }) + }) + }) +}) diff --git a/apps/datahub-e2e/src/e2e/organizations.cy.ts b/apps/datahub-e2e/src/e2e/organizations.cy.ts index b39da4ca39..ded2dc026d 100644 --- a/apps/datahub-e2e/src/e2e/organizations.cy.ts +++ b/apps/datahub-e2e/src/e2e/organizations.cy.ts @@ -77,14 +77,15 @@ describe('organizations', () => { }) describe('list features', () => { - it('should search with a filter on the selected org on click', () => { + it('should open the organization page', () => { cy.get('@organizationsName') .eq(10) .then(($clickedName) => { cy.get('@organizations').eq(10).click() - cy.url() - .should('include', 'publisher=') - .and('include', encodeURIComponent($clickedName.text().trim())) + cy.url().should( + 'contain', + `organization/${encodeURIComponent($clickedName.text().trim())}` + ) }) }) }) diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.css b/apps/datahub/src/app/organization/organization-details/organization-details.component.css index e69de29bb2..c0d50f2d1c 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.css +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.css @@ -0,0 +1,15 @@ +.list-page-dot { + width: 6px; + height: 6px; + border-radius: 6px; + position: relative; +} + +.list-page-dot:after { + content: ''; + position: absolute; + left: -7px; + top: -7px; + width: 20px; + height: 20px; +} diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.html b/apps/datahub/src/app/organization/organization-details/organization-details.component.html index 9ef5da0aef..d149ccd760 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.html +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.html @@ -10,6 +10,7 @@ <gn-ui-max-lines [maxLines]="2" *ngIf="organization.description"> <div> <gn-ui-markdown-parser + data-cy="organizationDescription" data-test="organizationDescription" [textContent]="organization.description" ></gn-ui-markdown-parser> @@ -26,6 +27,7 @@ class="w-[300px] flex flex-col gap-5" > <div + data-cy="organizationLogo" class="bg-white border border-[#d8d8d8] w-full h-[185px] rounded-lg p-[30px] -mt-28" > <gn-ui-thumbnail @@ -36,6 +38,7 @@ </gn-ui-thumbnail> </div> <a + data-cy="organizationDatasetCount" [routerLink]="['/', ROUTER_ROUTE_SEARCH]" [queryParams]="{ publisher: organization.name }" > @@ -50,6 +53,7 @@ </a> <a + data-cy="organizationEmail" data-test="organizationEmail" [href]="'mailto:' + organization.email" > @@ -93,6 +97,7 @@ [routerLink]="['/', ROUTER_ROUTE_SEARCH]" [queryParams]="{ publisher: organization.name }" class="gn-ui-btn-primary h-[34px] rounded-lg" + data-cy="organizationDetailsLastPublishedDatasetsSearchAllButton" data-test="organizationDetailsLastPublishedDatasetsSearchAllButton" > Search all @@ -111,6 +116,7 @@ > <div class="mb-4 flex flex-wrap gap-9 justify-center sm:justify-start px-[25px]" + data-cy="organizationPageLastPublishedDatasets" data-test="organizationPageLastPublishedDatasets" > <gn-ui-related-record-card @@ -119,6 +125,20 @@ [extraClass]="'w-[300px]'" ></gn-ui-related-record-card> </div> + <div + *ngIf="totalPages > 1" + class="flex flex-row justify-center gap-[14px] p-1 mx-auto" + [ngClass]="paginationContainerClass" + > + <button + *ngFor="let page of pages" + class="list-page-dot" + (click)="goToPage(page)" + [ngClass]=" + currentPage === page ? 'bg-primary' : 'bg-gray-400' + " + ></button> + </div> </ng-container> </ng-container> @@ -132,9 +152,9 @@ </ng-container> <ng-template #orgHasNoDataset> - <gn-ui-search-results-error + <gn-ui-error [type]="ErrorType.ORGANIZATION_HAS_NO_DATASET" - ></gn-ui-search-results-error> + ></gn-ui-error> </ng-template> </div> </div> diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts b/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts index 6e4d6078ac..1fff01d88f 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.spec.ts @@ -52,10 +52,6 @@ class OrganisationsServiceMock { } const anOrganizationWithManyDatasets: Organization = ORGANISATIONS_FIXTURE[0] -const anOrganizationWithOnlyOneDataset: Organization = { - ...ORGANISATIONS_FIXTURE[0], - recordCount: 1, -} const oneDataset = [DATASET_RECORDS[0]] const manyDatasets = DATASET_RECORDS.concat(DATASET_RECORDS[0]) diff --git a/apps/datahub/src/app/organization/organization-details/organization-details.component.ts b/apps/datahub/src/app/organization/organization-details/organization-details.component.ts index 23ab3fd7a2..ac116a314b 100644 --- a/apps/datahub/src/app/organization/organization-details/organization-details.component.ts +++ b/apps/datahub/src/app/organization/organization-details/organization-details.component.ts @@ -4,11 +4,13 @@ import { ChangeDetectorRef, Component, Input, + OnChanges, OnDestroy, OnInit, + SimpleChanges, ViewChild, } from '@angular/core' -import { AsyncPipe, NgForOf, NgIf } from '@angular/common' +import { AsyncPipe, NgClass, NgForOf, NgIf } from '@angular/common' import { CatalogRecord, Organization, @@ -32,7 +34,7 @@ import { } from '@geonetwork-ui/ui/elements' import { UiSearchModule } from '@geonetwork-ui/ui/search' import { SearchFacade } from '@geonetwork-ui/feature/search' -import { Observable, of, Subscription, switchMap } from 'rxjs' +import { BehaviorSubject, Observable, of, Subscription, switchMap } from 'rxjs' import { UiDatavizModule } from '@geonetwork-ui/ui/dataviz' import { RouterLink } from '@angular/router' import { ROUTER_ROUTE_SEARCH } from '@geonetwork-ui/feature/router' @@ -63,15 +65,19 @@ import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' UiDatavizModule, RouterLink, UiWidgetsModule, + NgClass, ], }) export class OrganizationDetailsComponent - implements OnInit, AfterViewInit, OnDestroy + implements OnInit, AfterViewInit, OnDestroy, OnChanges { protected readonly Error = Error protected readonly ErrorType = ErrorType + protected readonly ROUTER_ROUTE_SEARCH = ROUTER_ROUTE_SEARCH - @Input() organization: Organization + protected get pages() { + return new Array(this.totalPages).fill(0).map((_, i) => i + 1) + } lastPublishedDatasets$: Observable<CatalogRecord[]> = of([]) @@ -84,6 +90,11 @@ export class OrganizationDetailsComponent isFirstPage = this.currentPage === 1 isLastPage = false + organizationHasChanged$ = new BehaviorSubject<void>(undefined) + + @Input() organization: Organization + @Input() paginationContainerClass = 'w-full bottom-0 top-auto' + @ViewChild(BlockListComponent) list: BlockListComponent constructor( @@ -95,19 +106,29 @@ export class OrganizationDetailsComponent ngOnInit(): void { this.searchFacade.setPageSize(3) - this.lastPublishedDatasets$ = this.organizationsService - .getFiltersForOrgs([this.organization]) - .pipe( - switchMap((filters) => { - return this.searchFacade - .setFilters(filters) - .setSortBy(['desc', 'changeDate']).results$ - }) - ) + this.lastPublishedDatasets$ = this.organizationHasChanged$.pipe( + switchMap(() => { + return this.organizationsService + .getFiltersForOrgs([this.organization]) + .pipe( + switchMap((filters) => { + return this.searchFacade + .setFilters(filters) + .setSortBy(['desc', 'changeDate']).results$ + }) + ) + }) + ) this.manageSubscriptions() } + ngOnChanges(changes: SimpleChanges): void { + if (changes['organization']) { + this.organizationHasChanged$.next() + } + } + ngAfterViewInit() { // this is required to show the pagination correctly this.changeDetector.detectChanges() @@ -129,6 +150,10 @@ export class OrganizationDetailsComponent } } + goToPage(page: number) { + this.searchFacade.paginate(page) + } + private manageSubscriptions() { this.subscriptions$.add( this.searchFacade.isLoading$.subscribe( @@ -161,6 +186,4 @@ export class OrganizationDetailsComponent ) ) } - - protected readonly ROUTER_ROUTE_SEARCH = ROUTER_ROUTE_SEARCH } diff --git a/apps/datahub/src/app/organization/header-organization/organization-header.component.css b/apps/datahub/src/app/organization/organization-header/organization-header.component.css similarity index 100% rename from apps/datahub/src/app/organization/header-organization/organization-header.component.css rename to apps/datahub/src/app/organization/organization-header/organization-header.component.css diff --git a/apps/datahub/src/app/organization/header-organization/organization-header.component.html b/apps/datahub/src/app/organization/organization-header/organization-header.component.html similarity index 95% rename from apps/datahub/src/app/organization/header-organization/organization-header.component.html rename to apps/datahub/src/app/organization/organization-header/organization-header.component.html index 9c16ff9bc0..cd68f67fbd 100644 --- a/apps/datahub/src/app/organization/header-organization/organization-header.component.html +++ b/apps/datahub/src/app/organization/organization-header/organization-header.component.html @@ -22,6 +22,7 @@ </div> </div> <div + data-cy="organizationHeaderName" class="font-title text-[28px] max-w-screen-sm line-clamp-4 ml-4" [style.color]="foregroundColor" > @@ -41,6 +42,7 @@ <ng-container *ngIf="organization.website"> <p>•</p> <a + data-cy="organizationHeaderWebsiteLink" class="flex flex-row items-center gap-1" href="{{ organization.website.href }}" target="_blank" diff --git a/apps/datahub/src/app/organization/header-organization/organization-header.component.spec.ts b/apps/datahub/src/app/organization/organization-header/organization-header.component.spec.ts similarity index 100% rename from apps/datahub/src/app/organization/header-organization/organization-header.component.spec.ts rename to apps/datahub/src/app/organization/organization-header/organization-header.component.spec.ts diff --git a/apps/datahub/src/app/organization/header-organization/organization-header.component.ts b/apps/datahub/src/app/organization/organization-header/organization-header.component.ts similarity index 100% rename from apps/datahub/src/app/organization/header-organization/organization-header.component.ts rename to apps/datahub/src/app/organization/organization-header/organization-header.component.ts diff --git a/apps/datahub/src/app/organization/organization-page/organization-page.component.ts b/apps/datahub/src/app/organization/organization-page/organization-page.component.ts index a1dc77c981..99cc91b16b 100644 --- a/apps/datahub/src/app/organization/organization-page/organization-page.component.ts +++ b/apps/datahub/src/app/organization/organization-page/organization-page.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core' import { RouterFacade } from '@geonetwork-ui/feature/router' import { AsyncPipe, NgIf } from '@angular/common' -import { OrganizationHeaderComponent } from '../header-organization/organization-header.component' +import { OrganizationHeaderComponent } from '../organization-header/organization-header.component' import { OrganizationDetailsComponent } from '../organization-details/organization-details.component' import { combineLatest, Observable, of, switchMap } from 'rxjs' import { filter } from 'rxjs/operators' diff --git a/libs/common/domain/src/lib/model/user/user.model.ts b/libs/common/domain/src/lib/model/user/user.model.ts index fefd657049..591fc7bf18 100644 --- a/libs/common/domain/src/lib/model/user/user.model.ts +++ b/libs/common/domain/src/lib/model/user/user.model.ts @@ -5,6 +5,6 @@ export interface UserModel { name: string surname: string email: string - organization: string + organisation: string profileIcon?: string } diff --git a/libs/feature/catalog/src/lib/my-org/my-org.service.ts b/libs/feature/catalog/src/lib/my-org/my-org.service.ts index 324884f5a7..8840cc3bba 100644 --- a/libs/feature/catalog/src/lib/my-org/my-org.service.ts +++ b/libs/feature/catalog/src/lib/my-org/my-org.service.ts @@ -27,12 +27,12 @@ export class MyOrgService { this.orgService.organisations$, ]).pipe( map(([user, allUsers, orgs]) => { - const orgName = user.organization + const orgName = user.organisation const org = orgs.find((org) => org.name === orgName) const logoUrl = org?.logoUrl?.toString() const recordCount = org?.recordCount const userList = allUsers.filter( - (user) => user.organization === orgName + (user) => user.organisation === orgName ) const userCount = userList.length return { diff --git a/libs/ui/elements/src/lib/related-record-card/related-record-card.component.html b/libs/ui/elements/src/lib/related-record-card/related-record-card.component.html index 932c405b23..85fb920c14 100644 --- a/libs/ui/elements/src/lib/related-record-card/related-record-card.component.html +++ b/libs/ui/elements/src/lib/related-record-card/related-record-card.component.html @@ -1,5 +1,5 @@ <a - class="w-72 h-96 overflow-hidden rounded-lg bg-white cursor-pointer block hover:-translate-y-2 duration-[180ms]" + [class]="classList" [routerLink]="['/dataset', record.uniqueIdentifier]" target="_blank" > diff --git a/libs/ui/elements/src/lib/related-record-card/related-record-card.component.ts b/libs/ui/elements/src/lib/related-record-card/related-record-card.component.ts index f883b3c31b..c166ea575d 100644 --- a/libs/ui/elements/src/lib/related-record-card/related-record-card.component.ts +++ b/libs/ui/elements/src/lib/related-record-card/related-record-card.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, Input } from '@angular/core' +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' @Component({ @@ -8,5 +8,26 @@ import { CatalogRecord } from '@geonetwork-ui/common/domain/model/record' changeDetection: ChangeDetectionStrategy.OnPush, }) export class RelatedRecordCardComponent { + private readonly baseClasses: string + @Input() record: CatalogRecord + @Input() extraClass = '' + + constructor() { + this.baseClasses = [ + 'w-72', + 'h-96', + 'overflow-hidden', + 'rounded-lg', + 'bg-white', + 'cursor-pointer', + 'block', + 'hover:-translate-y-2 ', + 'duration-[180ms]', + ].join(' ') + } + + get classList() { + return `${this.baseClasses} ${this.extraClass}` + } } diff --git a/support-services/docker-entrypoint-initdb.d/dump b/support-services/docker-entrypoint-initdb.d/dump index 0a433c68ec163548b3136110c93fbcc891a0406e..a163457194378dbd2b49441095f6050896fe75bc 100644 GIT binary patch delta 677 zcmV;W0$TmZjU4Qb93fCgL`_fu4gdrQ0RaI30000O000002mk;801f~E000buA+;d^ zhN^%50RRAaoLy4OZrd;rJj-9Pw_Ga5PI_uDc3?Rv-~@rwB9~k!awQR=NP?s!_>=q( zz4^zwlpI(nAdriF%+3szud;x;%01R9MD!@33+O}k@sgQzJpL)(ey3pk7RyUG)|82! z8;sZTYF_4AUn_U>O5m;2I2&6ovp4j;5(s~<U8$7-iSSMU)+o#x)J|gfWP<x>*TD^J zxYSsQ6Xe_L`y6kTZIwq6l{IiUpK7|f#)PVG3eziZjlmX8oKl<id@{VAGDyh|_O||N z2=K(a7*gww1Pjc#Y?UFyJWm^Pnb0sW>5=Qak=0sakJ>snLgcdZywu2Q+x4vS6XSpF zq1f!<X}g>AzRMmuLmPP;p*sKdaNu2n7?k+bovF$5%C5ObHd0`snZP!OVh(ekJ)cj} zTW3jh(NS<dA#qF+_DYlY)7W#dpO1ad9=I<R!Y?DNcXpBoDgdWbkl_M^Z5=m95TrFK zQ-icx2Z#-ipiFRNI~*-hg&GY`2!VgW&L&+F5}Yx%AM@WpMX*&xKO9Ka(}m!?m7O5B zJytrsAR&>N{%8%}5RtUu3-rR4__dbS5(fi(B=i$PGqyuGwx!jy1+aB?KtDbzH}ayO zSMkU)(iXXh>vBZQl@W}Dl1Yy*_so&+S7TVRtBKKtAGf!|FtGn-$jJ}3SS$wq0V4|_ z42K1*0fz;w0*3{x1BV5y1cwEz1-AvP2A)|@PvL>$k;*YlhoSQ$G(Dfj{qc76R4k>) z1xxj*J8JUKQb4IKM2@Y{Se=S(?F8L8VGc6FrKf4jF>WZvvZ=>Cz-#~Ozk53DcDsM< L+NXh+@&yOhKI=VU delta 511 zcmV<b0RaB&jvUC193fCgL`_fu4gdrQ0RaI300002000005&!@I00;m8000AlA+;d^ zhN^#B0RRAaoK28RZi6rohS%m4-gSkQM-PxhAxR|?MWj_W*%&+!E5=5CkTY?I?zmWO zNLwUiM*lza%_E*81EP#IRNPq5z_2r*4Z59>4A1^6xqOCT_@db>96T<qrV{MGJ)hcC zh<|4>bquW=0h6-X3|&CCya0T5x!?t6fUkcZpd<%$2hlJv4^mKV)ygza@_czDhY@)> zU#Gk|+Hwtr<x1kL?<)VN0s>XMC9>sONeMMbJ4)-i?<B281-$5Rs;b`vgAdeM6H<46 z;Q|n4HJAQinuZQB3p6l<X`mv$vvS3t1)-W|K*Y>xnhPM6axE$KIPo@1)+s)0chg|G z7ii<8??`Kl%l=#AjJkk$^5P+!to+SOwW1oxFajy=BW#@|Q_K<_`_5|BD8F>#e9(8` zXACQ(T=?6MWyd6)mc645)j0?JAqx^x_NOIjeAlV`mwzKs6#W1uiME`FsjLBqsjLEr zsjLHssjLKtsjLOJsjLQ`Sx=Ag0r^PfNcoUydW5Fu)3`t0j-HC86uDrjK6OV;9$E@0 zwS~yB6&kBkv8|n;8z;;`M!57eZ8^pb#aK4=xCeOcfBkn)huv=X59^etewUU82iBYh B;s^i$