From 77f423f59561a06f1ada368376ec25e44f063471 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 7 May 2024 16:07:07 +0200 Subject: [PATCH 01/20] fix(record-api-form): prevent bug on slice if apiLink id undefined this can happen on app load as there is no ngIf which would bother the animation when opening the form --- .../lib/record-api-form/record-api-form.component.spec.ts | 8 ++++++++ .../src/lib/record-api-form/record-api-form.component.ts | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts index d49207bf23..53b718f21c 100644 --- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts @@ -122,4 +122,12 @@ describe('RecordApFormComponent', () => { ]) }) }) + describe('When apiLink input is undefined', () => { + it('should not call parseOutputFormats()', () => { + const spy = jest.spyOn(component, 'parseOutputFormats') + component.apiLink = undefined + fixture.detectChanges() + expect(spy).not.toHaveBeenCalled() + }) + }) }) diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts index 96f8f375ff..2377c20495 100644 --- a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts @@ -17,9 +17,11 @@ const DEFAULT_PARAMS = { }) export class RecordApiFormComponent { @Input() set apiLink(value: DatasetServiceDistribution) { - this.apiBaseUrl = value ? value.url.href : undefined this.outputFormats = [{ value: 'json', label: 'JSON' }] - this.parseOutputFormats() + if (value) { + this.apiBaseUrl = value.url.href + this.parseOutputFormats() + } this.resetUrl() } offset$ = new BehaviorSubject('') From 395b8d717f4a8f45c2dbaa551184c3cdb89d0715 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 7 May 2024 16:09:41 +0200 Subject: [PATCH 02/20] fix(data.service): resolve Promise with empty list when OGC API requests do not succeed this prevent download list with WFS links from breaking --- .../src/lib/service/data.service.spec.ts | 61 +++++++++++++------ .../dataviz/src/lib/service/data.service.ts | 33 +++++----- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/libs/feature/dataviz/src/lib/service/data.service.spec.ts b/libs/feature/dataviz/src/lib/service/data.service.spec.ts index 390d087a94..2748eeac7a 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.spec.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.spec.ts @@ -81,6 +81,13 @@ jest.mock('@camptocamp/ogc-client', () => ({ newEndpointCall(url) // to track endpoint creation } getCollectionInfo() { + if (this.url.indexOf('error.http') > -1) { + return Promise.reject({ + type: 'http', + info: 'Something went wrong', + httpStatus: 403, + }) + } return Promise.resolve({ bulkDownloadLinks: { json: 'http://json', csv: 'http://csv' }, }) @@ -457,30 +464,44 @@ describe('DataService', () => { }) describe('#getDownloadLinksFromOgcApiFeatures', () => { - it('returns links with formats for link', async () => { - const url = new URL('https://my.ogc.api/features') - const links = await service.getDownloadLinksFromOgcApiFeatures({ - name: 'mycollection', - url, - type: 'service', - accessServiceProtocol: 'ogcFeatures', - }) - expect(links).toEqual([ - { + describe('calling getDownloadLinksFromOgcApiFeatures() with a valid URL', () => { + it('returns links with formats for link', async () => { + const url = new URL('https://my.ogc.api/features') + const links = await service.getDownloadLinksFromOgcApiFeatures({ name: 'mycollection', - mimeType: 'application/json', - url: new URL('http://json'), - type: 'download', + url, + type: 'service', accessServiceProtocol: 'ogcFeatures', - }, - { + }) + expect(links).toEqual([ + { + name: 'mycollection', + mimeType: 'application/json', + url: new URL('http://json'), + type: 'download', + accessServiceProtocol: 'ogcFeatures', + }, + { + name: 'mycollection', + mimeType: 'text/csv', + url: new URL('http://csv'), + type: 'download', + accessServiceProtocol: 'ogcFeatures', + }, + ]) + }) + }) + describe('calling getDownloadLinksFromOgcApiFeatures() with a erroneous URL', () => { + it('returns empty list of links', async () => { + const url = new URL('http://error.http/ogcapi') + const links = await service.getDownloadLinksFromOgcApiFeatures({ name: 'mycollection', - mimeType: 'text/csv', - url: new URL('http://csv'), - type: 'download', + url, + type: 'service', accessServiceProtocol: 'ogcFeatures', - }, - ]) + }) + expect(links).toEqual([]) + }) }) }) diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index 708905c488..993a142933 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -156,20 +156,25 @@ export class DataService { async getDownloadLinksFromOgcApiFeatures( ogcApiLink: DatasetServiceDistribution ): Promise { - const collectionInfo = await this.getDownloadUrlsFromOgcApi( - ogcApiLink.url.href - ) - - return Object.keys(collectionInfo.bulkDownloadLinks).map((downloadLink) => { - return { - ...ogcApiLink, - type: 'download', - url: new URL(collectionInfo.bulkDownloadLinks[downloadLink]), - mimeType: getMimeTypeForFormat( - getFileFormatFromServiceOutput(downloadLink) - ), - } - }) + try { + const collectionInfo = await this.getDownloadUrlsFromOgcApi( + ogcApiLink.url.href + ) + return Object.keys(collectionInfo.bulkDownloadLinks).map( + (downloadLink) => { + return { + ...ogcApiLink, + type: 'download', + url: new URL(collectionInfo.bulkDownloadLinks[downloadLink]), + mimeType: getMimeTypeForFormat( + getFileFormatFromServiceOutput(downloadLink) + ), + } + } + ) + } catch (error) { + return Promise.resolve([]) + } } async getDownloadUrlsFromOgcApi(url: string): Promise { From 3371b676033bae088ee0423b96863a4eed463e2a Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Tue, 7 May 2024 16:10:56 +0200 Subject: [PATCH 03/20] fix(download-list): recover message that download link was generated by an API/WFS --- libs/common/fixtures/src/lib/link.fixtures.ts | 8 ++++++++ .../downloads-list/downloads-list.component.spec.ts | 12 ++++++++++++ .../lib/downloads-list/downloads-list.component.ts | 2 +- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/libs/common/fixtures/src/lib/link.fixtures.ts b/libs/common/fixtures/src/lib/link.fixtures.ts index 3a52ff12c2..c7c929a647 100644 --- a/libs/common/fixtures/src/lib/link.fixtures.ts +++ b/libs/common/fixtures/src/lib/link.fixtures.ts @@ -110,6 +110,14 @@ export const LINK_FIXTURES: Record = deepFreeze({ url: new URL('https://my.ogc.server/wfs'), accessServiceProtocol: 'wfs', }, + geodataWfsDownload: { + name: 'mylayer', + type: 'download', + url: new URL( + 'https://my.ogc.server/wfs?GetFeature&FeatureType=surval_parametre_ligne&format=csv' + ), + accessServiceProtocol: 'wfs', + }, geodataWms2: { name: 'myotherlayer', type: 'service', diff --git a/libs/ui/elements/src/lib/downloads-list/downloads-list.component.spec.ts b/libs/ui/elements/src/lib/downloads-list/downloads-list.component.spec.ts index 1abb05ee73..d3a0325a98 100644 --- a/libs/ui/elements/src/lib/downloads-list/downloads-list.component.spec.ts +++ b/libs/ui/elements/src/lib/downloads-list/downloads-list.component.spec.ts @@ -138,6 +138,18 @@ describe('DownloadsListComponent', () => { expect(items[0].componentInstance.isFromWfs).toEqual(false) }) }) + describe('displaying download links from WFS', () => { + let items: DebugElement[] + + beforeEach(() => { + component.links = [LINK_FIXTURES.geodataWfsDownload] + fixture.detectChanges() + items = de.queryAll(By.directive(MockDownloadItemComponent)) + }) + it('sets isFromWfs to true', () => { + expect(items[0].componentInstance.isFromWfs).toEqual(true) + }) + }) describe('filtering links', () => { beforeEach(() => { component.links = [ diff --git a/libs/ui/elements/src/lib/downloads-list/downloads-list.component.ts b/libs/ui/elements/src/lib/downloads-list/downloads-list.component.ts index 722fd71d74..4ba86d26e2 100644 --- a/libs/ui/elements/src/lib/downloads-list/downloads-list.component.ts +++ b/libs/ui/elements/src/lib/downloads-list/downloads-list.component.ts @@ -95,6 +95,6 @@ export class DownloadsListComponent { } isFromWfs(link: DatasetDistribution) { - return link.type === 'service' && link.accessServiceProtocol === 'wfs' + return link.type === 'download' && link.accessServiceProtocol === 'wfs' } } From a1d20328eb16dc677bfbde4ed1a619f6375d165c Mon Sep 17 00:00:00 2001 From: Romuald Caplier Date: Thu, 25 Apr 2024 16:21:36 +0200 Subject: [PATCH 04/20] otherLinks is now a list if there is 9 or more items --- apps/datahub/src/app/app.component.html | 2 +- .../record-apis/record-apis.component.html | 84 +++--- .../record-apis/record-apis.component.ts | 5 +- .../record-metadata.component.css | 15 - .../record-metadata.component.html | 15 +- .../record-otherlinks.component.html | 51 ++-- .../record-otherlinks.component.ts | 7 + .../record/src/lib/state/mdview.facade.ts | 16 +- libs/ui/elements/src/index.ts | 2 + .../lib/link-card/link-card.component.html | 2 +- .../src/lib/list-item/list-item.component.css | 0 .../lib/list-item/list-item.component.html | 18 ++ .../lib/list-item/list-item.component.spec.ts | 102 +++++++ .../list-item/list-item.component.stories.ts | 45 +++ .../src/lib/list-item/list-item.component.ts | 21 ++ .../elements/src/lib/list/list.component.css | 32 ++ .../elements/src/lib/list/list.component.html | 65 ++++ .../src/lib/list/list.component.spec.ts | 281 ++++++++++++++++++ .../elements/src/lib/list/list.component.ts | 200 +++++++++++++ .../ui/elements/src/lib/ui-elements.module.ts | 8 + .../src/lib/carousel/carousel.component.css | 3 + .../src/lib/carousel/carousel.component.html | 71 ++++- .../lib/carousel/carousel.component.spec.ts | 4 +- .../src/lib/carousel/carousel.component.ts | 79 ++++- 24 files changed, 996 insertions(+), 132 deletions(-) create mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.css create mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.html create mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.spec.ts create mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.stories.ts create mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.ts create mode 100644 libs/ui/elements/src/lib/list/list.component.css create mode 100644 libs/ui/elements/src/lib/list/list.component.html create mode 100644 libs/ui/elements/src/lib/list/list.component.spec.ts create mode 100644 libs/ui/elements/src/lib/list/list.component.ts diff --git a/apps/datahub/src/app/app.component.html b/apps/datahub/src/app/app.component.html index feeab422eb..a3388896aa 100644 --- a/apps/datahub/src/app/app.component.html +++ b/apps/datahub/src/app/app.component.html @@ -1,6 +1,6 @@
diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.html b/apps/datahub/src/app/record/record-apis/record-apis.component.html index ee0d3c6694..d6d3b243ff 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.html +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.html @@ -1,27 +1,17 @@ -
-

- record.metadata.api -

+ -
-
-
-
-
-

- record.metadata.api.form.title -

- +
+ record.metadata.api.form.closeButton +
+ close + + +
+
-
- + diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.ts b/apps/datahub/src/app/record/record-apis/record-apis.component.ts index 1473720d76..2ee7526939 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.ts +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.ts @@ -12,7 +12,10 @@ export class RecordApisComponent implements OnInit { maxHeight = '0px' opacity = 0 selectedApiLink: DatasetServiceDistribution - constructor(public facade: MdViewFacade) {} + + apiLinks$ = this.facade.apiLinks$ + + constructor(private facade: MdViewFacade) {} ngOnInit(): void { this.setStyle(undefined) diff --git a/apps/datahub/src/app/record/record-metadata/record-metadata.component.css b/apps/datahub/src/app/record/record-metadata/record-metadata.component.css index a92dcd44b6..e3a0105a26 100644 --- a/apps/datahub/src/app/record/record-metadata/record-metadata.component.css +++ b/apps/datahub/src/app/record/record-metadata/record-metadata.component.css @@ -33,18 +33,3 @@ .tab-header-label { @apply uppercase text-sm text-primary opacity-75 hover:text-primary-darker; } - -:host { - --container-outside-width: calc(50vw - 1024px / 2); -} -@media (max-width: 1024px) { - :host { - --container-outside-width: 1rem; - } -} - -:host ::ng-deep gn-ui-carousel { - display: block; - margin-left: calc(-1 * var(--container-outside-width)); - margin-right: calc(-1 * var(--container-outside-width)); -} 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 a339208c56..6855889c7c 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 @@ -117,19 +117,20 @@ class="container-lg px-4 lg:mx-auto" *ngIf="displayDownload$ | async" > - + @@ -149,7 +149,7 @@ -
+
+

record.metadata.links

-
- - - arrow_back - - - - - arrow_forward - - -
+
- - + + - + +
diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 2072d14edd..2086564c30 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -32,9 +32,6 @@ export class MdViewFacade { map((uuid) => !!uuid) ) - isLoading$ = this.store.pipe(select(MdViewSelectors.getMetadataIsLoading)) - - isMetadataLoading$ = this.store.pipe( select(MdViewSelectors.getMetadataIsLoading) ) diff --git a/libs/ui/elements/src/lib/list-item/list-item.component.spec.ts b/libs/ui/elements/src/lib/list-item/list-item.component.spec.ts index e3ba8439ac..0c528e3003 100644 --- a/libs/ui/elements/src/lib/list-item/list-item.component.spec.ts +++ b/libs/ui/elements/src/lib/list-item/list-item.component.spec.ts @@ -2,10 +2,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { MatIconModule } from '@angular/material/icon' import { TranslateModule } from '@ngx-translate/core' import { ListItemComponent } from './list-item.component' -import { By } from '@angular/platform-browser' -import { ChangeDetectionStrategy, DebugElement } from '@angular/core' +import { ChangeDetectionStrategy } from '@angular/core' -describe('DownloadsListItemComponent', () => { +describe('ListItemComponent', () => { let component: ListItemComponent let fixture: ComponentFixture @@ -38,65 +37,6 @@ describe('DownloadsListItemComponent', () => { 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) - }) - }) - }) + describe('download description', () => {}) }) diff --git a/libs/ui/elements/src/lib/list/list.component.html b/libs/ui/elements/src/lib/list/list.component.html index 9801cfac30..12335170c2 100644 --- a/libs/ui/elements/src/lib/list/list.component.html +++ b/libs/ui/elements/src/lib/list/list.component.html @@ -1,57 +1,7 @@ -
-
-
- {{ title }} -
-
- - > -
-
- - arrow_back - - - arrow_forward - -
-
- +
- +
+ + + > diff --git a/libs/ui/elements/src/lib/list/list.component.spec.ts b/libs/ui/elements/src/lib/list/list.component.spec.ts index 80db63b5ce..b970fa24a8 100644 --- a/libs/ui/elements/src/lib/list/list.component.spec.ts +++ b/libs/ui/elements/src/lib/list/list.component.spec.ts @@ -6,15 +6,10 @@ import { NO_ERRORS_SCHEMA, } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' -import { By } from '@angular/platform-browser' import { LinkClassifierService } from '@geonetwork-ui/util/shared' -import { LINK_FIXTURES } from '@geonetwork-ui/common/fixtures' import { TranslateModule } from '@ngx-translate/core' import { ListComponent } from './list.component' -import { - DatasetDistribution, - DatasetDownloadDistribution, -} from '@geonetwork-ui/common/domain/model/record' +import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record' @Component({ selector: 'gn-ui-download-item', @@ -27,7 +22,7 @@ class MockDownloadItemComponent { @Input() isFromWfs: boolean } -describe('DownloadsListComponent', () => { +describe('ListComponent', () => { let component: ListComponent let fixture: ComponentFixture let de: DebugElement @@ -56,226 +51,4 @@ describe('DownloadsListComponent', () => { fixture.detectChanges() expect(component).toBeTruthy() }) - - describe('with a non-empty list of downloads', () => { - let items: DebugElement[] - - beforeEach(() => { - component.listItems = [ - LINK_FIXTURES.dataCsv, - LINK_FIXTURES.dataPdf, - LINK_FIXTURES.dataPdf, - ] - fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) - }) - it('contains three links', () => { - expect(items.length).toBe(3) - }) - }) - - describe('with an empty list of downloads', () => { - let item: DebugElement - - beforeEach(() => { - component.listItems = [] - fixture.detectChanges() - item = de.query(By.directive(ListComponent)) - }) - it('should not display', () => { - expect(item).toBeNull() - }) - }) - - describe('when link format is unknown', () => { - let items: DebugElement[] - - beforeEach(() => { - component.listItems = [LINK_FIXTURES.unknownFormat] - fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) - }) - it('contains one link in "others" section', () => { - expect(items.length).toBe(1) - expect(component.isLinkOfFormat(component.listItems[0], 'others')).toBe( - true - ) - }) - }) - describe('when link mime type is unknown', () => { - let items: DebugElement[] - - beforeEach(() => { - component.listItems = [ - { - ...LINK_FIXTURES.geodataJsonWithMimeType, - mimeType: 'unknown/x-type', - } as DatasetDownloadDistribution, - ] - fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) - }) - it('contains one link and mime type is ignored', () => { - expect(items.length).toBe(1) - expect(component.isLinkOfFormat(component.listItems[0], 'json')).toBe( - true - ) - }) - }) - describe('derives color and format from link', () => { - let items: DebugElement[] - - beforeEach(() => { - component.listItems = [LINK_FIXTURES.geodataShpWithMimeType] - fixture.detectChanges() - items = de.queryAll(By.directive(MockDownloadItemComponent)) - }) - it('contains color, isWfs & format', () => { - expect(items.length).toBe(1) - expect(items[0].componentInstance.link).toEqual( - LINK_FIXTURES.geodataShpWithMimeType - ) - expect(items[0].componentInstance.format).toEqual('shp') - expect(items[0].componentInstance.color).toEqual( - expect.stringMatching(/#[0-9a-b]{2,6}/i) - ) - expect(items[0].componentInstance.isFromWfs).toEqual(false) - }) - }) - describe('filtering links', () => { - beforeEach(() => { - component.listItems = [ - LINK_FIXTURES.dataCsv, - LINK_FIXTURES.geodataJsonWithMimeType, - ] - }) - describe('no filter', () => { - beforeEach(() => { - component.activeFilterFormats = ['all'] - fixture.detectChanges() - }) - it('shows all links', () => { - expect(component.filteredListItems).toEqual([ - LINK_FIXTURES.dataCsv, - LINK_FIXTURES.geodataJsonWithMimeType, - ]) - }) - }) - describe('filter on csv', () => { - beforeEach(() => { - component.activeFilterFormats = ['csv'] - fixture.detectChanges() - }) - it('shows only one link', () => { - expect(component.filteredListItems).toEqual([LINK_FIXTURES.dataCsv]) - }) - }) - describe('filter on json and csv', () => { - beforeEach(() => { - component.activeFilterFormats = ['csv', 'json'] - fixture.detectChanges() - }) - it('shows both links including geojson', () => { - expect(component.filteredListItems).toEqual([ - LINK_FIXTURES.dataCsv, - LINK_FIXTURES.geodataJsonWithMimeType, - ]) - }) - }) - describe('filter on shp', () => { - beforeEach(() => { - component.activeFilterFormats = ['shp'] - fixture.detectChanges() - }) - it('shows no link', () => { - expect(component.filteredListItems).toEqual([]) - }) - }) - - describe('toggling formats', () => { - it('removes already enabled formats', () => { - component.activeFilterFormats = ['excel', 'csv', 'shp'] - component.toggleFilterFormat('excel') - expect(component.activeFilterFormats).toEqual(['csv', 'shp']) - }) - it('adds disabled formats', () => { - component.activeFilterFormats = ['excel', 'csv', 'shp'] - component.toggleFilterFormat('json') - expect(component.activeFilterFormats).toEqual([ - 'excel', - 'csv', - 'shp', - 'json', - ]) - }) - it('sets filter to all if disabling the last format', () => { - component.activeFilterFormats = ['excel', 'csv'] - component.toggleFilterFormat('excel') - component.toggleFilterFormat('csv') - expect(component.activeFilterFormats).toEqual(['all']) - }) - it('toggling all disables other formats if disabled', () => { - component.activeFilterFormats = ['excel', 'csv'] - component.toggleFilterFormat('all') - expect(component.activeFilterFormats).toEqual(['all']) - }) - it('toggling all does nothing if already enabled', () => { - component.activeFilterFormats = ['all'] - component.toggleFilterFormat('all') - expect(component.activeFilterFormats).toEqual(['all']) - }) - }) - }) - - describe('filter buttons visibility', () => { - let items: DebugElement[] - describe('csv, json, pdf', () => { - beforeEach(() => { - component.listItems = [ - LINK_FIXTURES.dataCsv, - LINK_FIXTURES.dataJson, - LINK_FIXTURES.dataPdf, - ] - fixture.detectChanges() - items = de.queryAll(By.css('.format-filter')) - }) - it('show only all, csv, json and pdf filters', () => { - const displayedFormats = items.map( - (item) => item.attributes['data-format'] - ) - expect(displayedFormats).toEqual(['all', 'csv', 'json', 'others']) - }) - }) - describe('geojson, shp, excel', () => { - beforeEach(() => { - component.listItems = [ - LINK_FIXTURES.geodataJsonWithMimeType, - LINK_FIXTURES.geodataShp, - LINK_FIXTURES.dataXls, - LINK_FIXTURES.dataXlsx, - ] - fixture.detectChanges() - items = de.queryAll(By.css('.format-filter')) - }) - it('show only all, excel, json and shp filters', () => { - const displayedFormats = items.map( - (item) => item.attributes['data-format'] - ) - expect(displayedFormats).toEqual(['all', 'excel', 'json', 'shp']) - }) - }) - describe('pdf', () => { - beforeEach(() => { - component.listItems = [LINK_FIXTURES.dataPdf] - fixture.detectChanges() - items = de.queryAll(By.css('.format-filter')) - }) - it('show only all and others filters', () => { - const displayedFormats = items.map( - (item) => item.attributes['data-format'] - ) - expect(displayedFormats).toEqual(['all', 'others']) - }) - }) - }) }) diff --git a/libs/ui/elements/src/lib/list/list.component.ts b/libs/ui/elements/src/lib/list/list.component.ts index a69b49cc08..6b7f2e318e 100644 --- a/libs/ui/elements/src/lib/list/list.component.ts +++ b/libs/ui/elements/src/lib/list/list.component.ts @@ -8,13 +8,9 @@ import { ViewChild, } from '@angular/core' import { TranslateService } from '@ngx-translate/core' -import { getBadgeColor, getFileFormat } from '@geonetwork-ui/util/shared' import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record' import { MatPaginator, PageEvent } from '@angular/material/paginator' -const FILTER_FORMATS = ['all', 'csv', 'excel', 'json', 'shp', 'others'] as const -type FilterFormat = typeof FILTER_FORMATS[number] - @Component({ selector: 'gn-ui-list', templateUrl: './list.component.html', @@ -32,26 +28,13 @@ export class ListComponent implements OnChanges { listLength = 1 listPages = [] - activeFilterFormats: FilterFormat[] = ['all'] - - @Input() previousButtonWidth = 1.4 - @Input() previousButtonHeight = 1.4 - previousButtonStyle = '' - - @Input() nextButtonWidth = 1.4 - @Input() nextButtonHeight = 1.4 - nextButtonStyle = '' - isFirstPage = true isLastPage = false constructor( private readonly translateService: TranslateService, private readonly changeDetector: ChangeDetectorRef - ) { - this.previousButtonStyle = `width: ${this.previousButtonWidth}rem; height: ${this.previousButtonHeight}rem;` - this.nextButtonStyle = `width: ${this.nextButtonWidth}rem; height: ${this.nextButtonHeight}rem;` - } + ) {} ngOnChanges(changes: SimpleChanges): void { const listItems = changes['listItems'] @@ -116,85 +99,10 @@ export class ListComponent implements OnChanges { this.isLastPage = this.currentPage === this.listPages.length - 1 } - // TODO <--------------------------------------------------------------------- TODO: - get filteredListItems(): DatasetDistribution[] { const startIndex = this.currentPage * this.listPageSize const endIndex = startIndex + this.listPageSize - return this.listItems - .filter((link) => - this.activeFilterFormats.some((format) => - this.isLinkOfFormat(link, format) - ) - ) - .slice(startIndex, endIndex) - } - - get visibleFormats(): FilterFormat[] { - return FILTER_FORMATS.filter((format) => - this.listItems.some((link) => this.isLinkOfFormat(link, format)) - ) - } - - toggleFilterFormat(format: FilterFormat): void { - if (format === 'all') { - this.activeFilterFormats = ['all'] - return - } - if (this.isFilterActive(format)) { - this.activeFilterFormats = this.activeFilterFormats.filter( - (f: string) => format !== f - ) - } else { - this.activeFilterFormats = [ - ...this.activeFilterFormats.filter((f) => f !== 'all'), - format, - ] - } - if (this.activeFilterFormats.length === 0) { - this.activeFilterFormats = ['all'] - } - } - - isFilterActive(filter: FilterFormat): boolean { - return this.activeFilterFormats.includes(filter) - } - - getFilterFormatTitle(format: FilterFormat) { - if (format === 'all' || format === 'others') { - return this.translateService.instant(`datahub.search.filter.${format}`) - } - return format - } - - isLinkOfFormat(link: DatasetDistribution, format: FilterFormat): boolean { - if (format === 'all') { - return true - } - if (getFileFormat(link) === null) { - return format === 'others' - } - if (format === 'others') { - const knownFormats = FILTER_FORMATS.filter( - (format) => format !== 'all' && format !== 'others' - ) - return knownFormats.every( - (knownFormat) => !getFileFormat(link).includes(knownFormat) - ) - } - return getFileFormat(link).includes(format) - } - - getLinkFormat(link: DatasetDistribution) { - return getFileFormat(link) - } - - getLinkColor(link: DatasetDistribution) { - return getBadgeColor(getFileFormat(link)) - } - - isFromWfs(link: DatasetDistribution) { - return link.type === 'service' && link.accessServiceProtocol === 'wfs' + return this.listItems.slice(startIndex, endIndex) } } diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html new file mode 100644 index 0000000000..9203615ca4 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html @@ -0,0 +1,34 @@ +
+ + + arrow_back + + + + + arrow_forward + + +
diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts new file mode 100644 index 0000000000..4722325bc7 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.spec.ts @@ -0,0 +1,73 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MatIconModule } from '@angular/material/icon' + +import { PreviousNextButtonsComponent } from './previous-next-buttons.component' +import { TranslateModule } from '@ngx-translate/core' +import { By } from '@angular/platform-browser' +import { DebugElement } from '@angular/core' + +describe('PreviousNextButtonsComponent', () => { + let component: PreviousNextButtonsComponent + let fixture: ComponentFixture + let compiled: DebugElement + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + MatIconModule, + PreviousNextButtonsComponent, + TranslateModule.forRoot(), + ], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(PreviousNextButtonsComponent) + component = fixture.componentInstance + compiled = fixture.debugElement + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('onFirstElement', () => { + beforeEach(() => { + component.isFirst = true + component.isLast = false + fixture.detectChanges() + }) + + it('previous button should be disabled', () => { + const previousButton = compiled.query( + By.css('[data-test="previousButton"]') + ) + expect(previousButton.attributes['ng-reflect-disabled']).toEqual('true') + }) + + it("next button shouldn't be disabled", () => { + const nextButton = compiled.query(By.css('[data-test="nextButton"]')) + expect(nextButton.attributes['ng-reflect-disabled']).toEqual('false') + }) + }) + + describe('onLastElement', () => { + beforeEach(() => { + component.isFirst = false + component.isLast = true + fixture.detectChanges() + }) + + it('previous button should be disabled', () => { + const previousButton = compiled.query( + By.css('[data-test="previousButton"]') + ) + expect(previousButton.attributes['ng-reflect-disabled']).toEqual('false') + }) + + it("next button shouldn't be disabled", () => { + const nextButton = compiled.query(By.css('[data-test="nextButton"]')) + expect(nextButton.attributes['ng-reflect-disabled']).toEqual('true') + }) + }) +}) diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts new file mode 100644 index 0000000000..efc14c7116 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.stories.ts @@ -0,0 +1,39 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular' +import { PreviousNextButtonsComponent } from './previous-next-buttons.component' +import { TranslateModule } from '@ngx-translate/core' +import { MatIconModule } from '@angular/material/icon' +import { + TRANSLATE_DEFAULT_CONFIG, + UtilI18nModule, +} from '@geonetwork-ui/util/i18n' + +export default { + title: 'Inputs/PreviousNextButtonsComponent', + component: PreviousNextButtonsComponent, + parameters: { + backgrounds: { + default: 'dark', + }, + }, + decorators: [ + moduleMetadata({ + imports: [ + UtilI18nModule, + TranslateModule.forRoot(TRANSLATE_DEFAULT_CONFIG), + MatIconModule, + ], + }), + ], +} as Meta + +export const Primary: StoryObj = { + args: { + isFirst: true, + isLast: false, + }, + render: (args) => ({ + props: args, + template: + '', + }), +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts new file mode 100644 index 0000000000..3546968262 --- /dev/null +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.ts @@ -0,0 +1,32 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core' +import { ButtonComponent } from '../button/button.component' +import { MatIconModule } from '@angular/material/icon' + +@Component({ + selector: 'gn-ui-previous-next-buttons', + templateUrl: './previous-next-buttons.component.html', + styleUrls: ['./previous-next-buttons.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [ButtonComponent, MatIconModule], +}) +export class PreviousNextButtonsComponent { + @Input() isFirst: boolean + @Input() isLast: boolean + + @Output() directionButtonClicked: EventEmitter = new EventEmitter() + + previousButtonClicked() { + this.directionButtonClicked.next('previous') + } + + nextButtonClicked() { + this.directionButtonClicked.next('next') + } +} diff --git a/libs/ui/inputs/src/lib/ui-inputs.module.ts b/libs/ui/inputs/src/lib/ui-inputs.module.ts index f96f975288..caa3cf44a9 100644 --- a/libs/ui/inputs/src/lib/ui-inputs.module.ts +++ b/libs/ui/inputs/src/lib/ui-inputs.module.ts @@ -33,6 +33,7 @@ import { MatDatepickerModule } from '@angular/material/datepicker' import { MatNativeDateModule } from '@angular/material/core' import { EditableLabelDirective } from './editable-label/editable-label.directive' import { ImageInputComponent } from './image-input/image-input.component' +import { PreviousNextButtonsComponent } from './previous-next-buttons/previous-next-buttons.component' @NgModule({ declarations: [ @@ -73,6 +74,7 @@ import { ImageInputComponent } from './image-input/image-input.component' ImageInputComponent, DropdownSelectorComponent, DateRangePickerComponent, + PreviousNextButtonsComponent, ], exports: [ DropdownSelectorComponent, @@ -93,6 +95,7 @@ import { ImageInputComponent } from './image-input/image-input.component' DateRangePickerComponent, EditableLabelDirective, ImageInputComponent, + PreviousNextButtonsComponent, ], }) export class UiInputsModule {} diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.html b/libs/ui/layout/src/lib/carousel/carousel.component.html index da0fca219f..1252c9de0c 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.html +++ b/libs/ui/layout/src/lib/carousel/carousel.component.html @@ -1,36 +1,4 @@
-
-
- {{ title }} -
-
- - arrow_back - - - arrow_forward - -
-
-
+
+
+
+ +
diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts b/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts index a78239fd6c..7301cb0d1a 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts +++ b/libs/ui/layout/src/lib/carousel/carousel.component.spec.ts @@ -34,7 +34,7 @@ jest.mock('embla-carousel', () => { }) @Component({ - template: ` + template: `
`, }) @@ -49,7 +49,8 @@ describe('CarouselComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [CarouselWrapperComponent, CarouselComponent], + declarations: [CarouselWrapperComponent], + imports: [CarouselComponent], }).compileComponents() fixture = TestBed.createComponent(CarouselWrapperComponent) component = fixture.debugElement.query( @@ -82,4 +83,35 @@ describe('CarouselComponent', () => { }) }) }) + + describe('currentStepChange', () => { + it('emits the current step index', () => { + const spy = jest.fn() + component.currentStepChange.subscribe(spy) + component.scrollToStep(2) + expect(spy).toHaveBeenCalledWith(2) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('isFirstStep', () => { + it('returns true if the current step is the first one', () => { + expect(component.isFirstStep).toBe(true) + }) + it('returns false if the current step is not the first one', () => { + component.scrollToStep(2) + expect(component.isFirstStep).toBe(false) + }) + }) + + describe('isLastStep', () => { + it('returns true if the current step is the last one', () => { + component.scrollToStep(3) + expect(component.isLastStep).toBe(true) + }) + it('returns false if the current step is not the last one', () => { + component.scrollToStep(1) + expect(component.isLastStep).toBe(false) + }) + }) }) diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.stories.ts b/libs/ui/layout/src/lib/carousel/carousel.component.stories.ts index d777c901e8..c24a9d5a04 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.stories.ts +++ b/libs/ui/layout/src/lib/carousel/carousel.component.stories.ts @@ -8,7 +8,8 @@ const meta: Meta = { decorators: [ componentWrapperDecorator( (story) => - `
${story}
` + `

Please note that the carousel will overflow by default; to hide its items, make its container overflow-hidden!

+
${story}
` ), ], } @@ -20,15 +21,15 @@ export const Primary: Story = { render: (args) => ({ props: args, template: ` - +
First box
Second box
-
- Third box +
+ Third box (resize me!)
Fourth box diff --git a/libs/ui/layout/src/lib/carousel/carousel.component.ts b/libs/ui/layout/src/lib/carousel/carousel.component.ts index 85c292f02b..abb751af2a 100644 --- a/libs/ui/layout/src/lib/carousel/carousel.component.ts +++ b/libs/ui/layout/src/lib/carousel/carousel.component.ts @@ -10,37 +10,44 @@ import { ViewChild, } from '@angular/core' import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel' +import { CommonModule } from '@angular/common' @Component({ selector: 'gn-ui-carousel', templateUrl: './carousel.component.html', styleUrls: ['./carousel.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule], }) export class CarouselComponent implements AfterViewInit { @ViewChild('carouselOverflowContainer') carouselOverflowContainer: ElementRef @Input() containerClass = '' - @Input() stepsContainerClass = '' + @Input() stepsContainerClass = 'w-full bottom-0 top-auto' + @Output() currentStepChange = new EventEmitter() - @Output() stepsChanged = new EventEmitter() + protected steps: number[] = [] + protected emblaApi: EmblaCarouselType + protected currentStep = 0 - steps: number[] = [] - currentStep = 0 - emblaApi: EmblaCarouselType - - isFirstStep = true - isLastStep = false - - refreshSteps = () => { + protected refreshSteps = () => { this.steps = this.emblaApi.scrollSnapList() this.currentStep = this.emblaApi.selectedScrollSnap() - this.isFirstStep = this.currentStep === 0 - this.isLastStep = this.currentStep === this.steps.length - 1 - this.stepsChanged.emit(this.steps.length) + this.currentStepChange.emit(this.currentStep) this.changeDetector.detectChanges() } + get isFirstStep() { + return this.currentStep === 0 + } + get isLastStep() { + return this.currentStep === this.steps.length - 1 + } + get stepsCount() { + return this.steps.length + } + constructor(private changeDetector: ChangeDetectorRef) {} ngAfterViewInit() { @@ -57,21 +64,15 @@ export class CarouselComponent implements AfterViewInit { .on('select', this.refreshSteps) } - scrollToStep(stepIndex: number) { + public scrollToStep(stepIndex: number) { this.emblaApi.scrollTo(stepIndex) } - /** - * Click on previous arrow - */ public slideToPrevious() { if (this.isFirstStep) return this.emblaApi.scrollPrev() } - /** - * Click on next arrow - */ public slideToNext() { if (this.isLastStep) return this.emblaApi.scrollNext() diff --git a/libs/ui/layout/src/lib/ui-layout.module.ts b/libs/ui/layout/src/lib/ui-layout.module.ts index 69a28b9c8b..5d22fc9ce9 100644 --- a/libs/ui/layout/src/lib/ui-layout.module.ts +++ b/libs/ui/layout/src/lib/ui-layout.module.ts @@ -15,14 +15,12 @@ import { CarouselComponent } from './carousel/carousel.component' StickyHeaderComponent, AnchorLinkDirective, ExpandablePanelButtonComponent, - CarouselComponent, ], exports: [ ExpandablePanelComponent, StickyHeaderComponent, AnchorLinkDirective, ExpandablePanelButtonComponent, - CarouselComponent, ], }) export class UiLayoutModule {} From ae87f68e254d840ce5261fd9c17490aec64db3ec Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Wed, 8 May 2024 11:39:48 +0200 Subject: [PATCH 09/20] feat(ui): add block-list component This component offers simple pagination on a list of blocks --- apps/datahub/src/app/app.module.ts | 7 +- libs/ui/layout/src/index.ts | 1 + .../lib/block-list/block-list.component.css | 23 +++ .../lib/block-list/block-list.component.html | 15 ++ .../block-list/block-list.component.spec.ts | 150 ++++++++++++++++++ .../block-list.component.stories.ts | 44 +++++ .../lib/block-list/block-list.component.ts | 76 +++++++++ 7 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 libs/ui/layout/src/lib/block-list/block-list.component.css create mode 100644 libs/ui/layout/src/lib/block-list/block-list.component.html create mode 100644 libs/ui/layout/src/lib/block-list/block-list.component.spec.ts create mode 100644 libs/ui/layout/src/lib/block-list/block-list.component.stories.ts create mode 100644 libs/ui/layout/src/lib/block-list/block-list.component.ts diff --git a/apps/datahub/src/app/app.module.ts b/apps/datahub/src/app/app.module.ts index 4905fc5500..2baf778115 100644 --- a/apps/datahub/src/app/app.module.ts +++ b/apps/datahub/src/app/app.module.ts @@ -30,7 +30,11 @@ import { UiElementsModule, } from '@geonetwork-ui/ui/elements' import { UiInputsModule } from '@geonetwork-ui/ui/inputs' -import { CarouselComponent, UiLayoutModule } from '@geonetwork-ui/ui/layout' +import { + BlockListComponent, + CarouselComponent, + UiLayoutModule, +} from '@geonetwork-ui/ui/layout' import { UiSearchModule } from '@geonetwork-ui/ui/search' import { getGlobalConfig, @@ -153,6 +157,7 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] UiWidgetsModule, LinkCardComponent, CarouselComponent, + BlockListComponent, ], providers: [ importProvidersFrom(FeatureAuthModule), diff --git a/libs/ui/layout/src/index.ts b/libs/ui/layout/src/index.ts index 9dbf8aec71..358c081b9f 100644 --- a/libs/ui/layout/src/index.ts +++ b/libs/ui/layout/src/index.ts @@ -6,4 +6,5 @@ export * from './lib/form-field-wrapper/form-field-wrapper.component' export * from './lib/interactive-table/interactive-table-column/interactive-table-column.component' export * from './lib/interactive-table/interactive-table.component' export * from './lib/sticky-header/sticky-header.component' +export * from './lib/block-list/block-list.component' export * from './lib/ui-layout.module' diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.css b/libs/ui/layout/src/lib/block-list/block-list.component.css new file mode 100644 index 0000000000..0d37f18585 --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.css @@ -0,0 +1,23 @@ +:host .block-list-container ::ng-deep > * { + flex-shrink: 0; +} + +:host { + position: relative; +} + +.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/libs/ui/layout/src/lib/block-list/block-list.component.html b/libs/ui/layout/src/lib/block-list/block-list.component.html new file mode 100644 index 0000000000..56223f920f --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.html @@ -0,0 +1,15 @@ +
+ +
+
+ +
diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts new file mode 100644 index 0000000000..f03b68c977 --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.spec.ts @@ -0,0 +1,150 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { BlockListComponent } from './block-list.component' +import { Component, Input } from '@angular/core' +import { By } from '@angular/platform-browser' + +@Component({ + template: ` +
+
`, +}) +class BlockListWrapperComponent { + @Input() blocks = [1, 2, 3, 4, 5, 6, 7] +} + +describe('BlockListComponent', () => { + let component: BlockListComponent + let fixture: ComponentFixture + let blockEls: HTMLElement[] + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [BlockListWrapperComponent], + imports: [BlockListComponent], + }).compileComponents() + fixture = TestBed.createComponent(BlockListWrapperComponent) + component = fixture.debugElement.query( + By.directive(BlockListComponent) + ).componentInstance + fixture.detectChanges() + blockEls = fixture.debugElement + .queryAll(By.css('.block')) + .map((el) => el.nativeElement) + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + describe('pages computation', () => { + it('shows 5 items per page initially', () => { + const blocksVisibility = blockEls.map((el) => el.style.display !== 'none') + expect(blocksVisibility).toEqual([ + true, + true, + true, + true, + true, + false, + false, + ]) + }) + describe('click on step', () => { + beforeEach(() => { + component.goToPage(1) + }) + it('updates visibility', () => { + const blocksVisibility = blockEls.map( + (el) => el.style.display !== 'none' + ) + expect(blocksVisibility).toEqual([ + false, + false, + false, + false, + false, + true, + true, + ]) + }) + it('emits the selected page', () => { + expect(component['currentPage']).toEqual(1) + }) + }) + describe('custom page size', () => { + beforeEach(() => { + component.pageSize = 3 + component.goToPage(3) + fixture.detectChanges() + }) + it('updates visibility', () => { + const blocksVisibility = blockEls.map( + (el) => el.style.display !== 'none' + ) + expect(blocksVisibility).toEqual([ + false, + false, + false, + false, + false, + false, + true, + ]) + }) + }) + }) + + describe('previousPage', () => { + beforeEach(() => { + component.pageSize = 2 + component.goToPage(2) + component.previousPage() + }) + it('changes to previous page', () => { + expect(component['currentPage']).toEqual(1) + }) + }) + + describe('nextPage', () => { + beforeEach(() => { + component.pageSize = 2 + component.goToPage(1) + component.nextPage() + }) + it('changes to next page', () => { + expect(component['currentPage']).toEqual(2) + }) + }) + + describe('isFirstPage', () => { + beforeEach(() => { + component.pageSize = 3 + }) + it('returns true if the current page is the first one', () => { + expect(component.isFirstPage).toBe(true) + }) + it('returns false if the current page is not the first one', () => { + component.goToPage(1) + expect(component.isFirstPage).toBe(false) + }) + }) + + describe('isLastPage', () => { + beforeEach(() => { + component.pageSize = 3 + }) + it('returns true if the current page is the last one', () => { + component.goToPage(2) + expect(component.isLastPage).toBe(true) + }) + it('returns false if the current page is not the last one', () => { + component.goToPage(1) + expect(component.isLastPage).toBe(false) + }) + }) +}) diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts new file mode 100644 index 0000000000..31f06fdd74 --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.stories.ts @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook/angular' +import { BlockListComponent } from './block-list.component' +import { componentWrapperDecorator } from '@storybook/angular' + +const meta: Meta = { + component: BlockListComponent, + title: 'Layout/BlockListComponent', + decorators: [ + componentWrapperDecorator( + (story) => + `
${story}
` + ), + ], +} +export default meta +type Story = StoryObj< + BlockListComponent & { + blockCount: number + } +> + +export const Primary: Story = { + args: { + pageSize: 5, + blockCount: 17, + }, + render: (args) => ({ + props: { + ...args, + blockList: new Array(args.blockCount).fill(0).map((_, i) => i + 1), + }, + template: ` + +
+ Box {{ block }} +
+
+`, + }), +} diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.ts b/libs/ui/layout/src/lib/block-list/block-list.component.ts new file mode 100644 index 0000000000..a20ae957f0 --- /dev/null +++ b/libs/ui/layout/src/lib/block-list/block-list.component.ts @@ -0,0 +1,76 @@ +import { + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + ElementRef, + Input, + QueryList, +} from '@angular/core' +import { CommonModule } from '@angular/common' + +@Component({ + selector: 'gn-ui-block-list', + templateUrl: './block-list.component.html', + styleUrls: ['./block-list.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [CommonModule], +}) +export class BlockListComponent implements AfterContentInit { + @Input() pageSize = 5 + @Input() containerClass = '' + @Input() paginationContainerClass = 'w-full bottom-0 top-auto' + @ContentChildren('block', { read: ElementRef }) blocks: QueryList< + ElementRef + > + + protected currentPage = 0 + protected get pages() { + return new Array(this.pagesCount).fill(0).map((_, i) => i) + } + + get isFirstPage() { + return this.currentPage === 0 + } + get isLastPage() { + return this.currentPage === this.pagesCount - 1 + } + get pagesCount() { + return this.blocks ? Math.ceil(this.blocks.length / this.pageSize) : 1 + } + + constructor(private changeDetector: ChangeDetectorRef) {} + + ngAfterContentInit() { + this.blocks.changes.subscribe(this.refreshBlocksVisibility) + this.refreshBlocksVisibility() + } + + protected refreshBlocksVisibility = () => { + this.blocks.forEach((block, index) => { + block.nativeElement.style.display = + index >= this.currentPage * this.pageSize && + index < (this.currentPage + 1) * this.pageSize + ? null + : 'none' + }) + } + + public goToPage(index: number) { + this.currentPage = Math.max(Math.min(index, this.pagesCount - 1), 0) + this.changeDetector.detectChanges() + this.refreshBlocksVisibility() + } + + public previousPage() { + if (this.isFirstPage) return + this.goToPage(this.currentPage - 1) + } + + public nextPage() { + if (this.isLastPage) return + this.goToPage(this.currentPage + 1) + } +} From 5b0dee878e73750e951be1afdac2f356fbb4c5eb Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Wed, 8 May 2024 23:56:23 +0200 Subject: [PATCH 10/20] feat(dh): adapt to work with ui components --- .../record-apis/record-apis.component.html | 10 ++--- .../record-apis/record-apis.component.ts | 18 +++++---- .../record-otherlinks.component.html | 26 ++++++------- .../record-otherlinks.component.ts | 37 +++++++++++++++---- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.html b/apps/datahub/src/app/record/record-apis/record-apis.component.html index 83ecac8a4b..61cec4c40e 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.html +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.html @@ -6,23 +6,19 @@ record.metadata.api

- + 3" + *ngIf="hasPagination" [isFirst]="isFirstStepOrPage" [isLast]="isLastStepOrPage" (directionButtonClicked)="changeStepOrPage($event)" >
- - - + + + + - + > diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts index 01f5a8b613..97a39cf5a8 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.ts @@ -1,7 +1,12 @@ -import { Component, ChangeDetectionStrategy, ViewChild } from '@angular/core' +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ViewChild, +} from '@angular/core' import { MdViewFacade } from '@geonetwork-ui/feature/record' -import { CarouselComponent } from '@geonetwork-ui/ui/layout' -import { ListComponent } from '@geonetwork-ui/ui/elements' +import { BlockListComponent, CarouselComponent } from '@geonetwork-ui/ui/layout' @Component({ selector: 'datahub-record-otherlinks', @@ -9,13 +14,16 @@ import { ListComponent } from '@geonetwork-ui/ui/elements' styleUrls: ['./record-otherlinks.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RecordOtherlinksComponent { +export class RecordOtherlinksComponent implements AfterViewInit { otherLinks$ = this.facade.otherLinks$ @ViewChild(CarouselComponent) carousel: CarouselComponent - @ViewChild(ListComponent) list: ListComponent + @ViewChild(BlockListComponent) list: BlockListComponent - constructor(public facade: MdViewFacade) {} + constructor( + public facade: MdViewFacade, + private changeDetector: ChangeDetectorRef + ) {} get isFirstStepOrPage() { return this.carousel?.isFirstStep ?? this.list?.isFirstPage ?? true @@ -25,13 +33,26 @@ export class RecordOtherlinksComponent { return this.carousel?.isLastStep ?? this.list?.isLastPage ?? false } + get hasPagination() { + return (this.carousel?.stepsCount || this.list?.pagesCount) > 1 + } + changeStepOrPage(direction: string) { if (direction === 'next') { - this.list?.goToNextPage() + this.list?.nextPage() this.carousel?.slideToNext() } else { this.carousel?.slideToPrevious() - this.list?.goToPreviousPage() + this.list?.previousPage() } } + + updateView() { + this.changeDetector.detectChanges() + } + + ngAfterViewInit() { + // this is required to show the pagination correctly + this.changeDetector.detectChanges() + } } From b3c908b31fa8ef3607e062f25f0729b9c20e5fec Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 9 May 2024 00:07:36 +0200 Subject: [PATCH 11/20] chore: remove unused list & list-item components --- libs/ui/elements/src/index.ts | 2 - .../src/lib/list-item/list-item.component.css | 0 .../lib/list-item/list-item.component.html | 18 --- .../lib/list-item/list-item.component.spec.ts | 42 ------- .../list-item/list-item.component.stories.ts | 45 -------- .../src/lib/list-item/list-item.component.ts | 21 ---- .../elements/src/lib/list/list.component.css | 32 ------ .../elements/src/lib/list/list.component.html | 27 ----- .../src/lib/list/list.component.spec.ts | 54 --------- .../elements/src/lib/list/list.component.ts | 108 ------------------ .../ui/elements/src/lib/ui-elements.module.ts | 6 - 11 files changed, 355 deletions(-) delete mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.css delete mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.html delete mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.spec.ts delete mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.stories.ts delete mode 100644 libs/ui/elements/src/lib/list-item/list-item.component.ts delete mode 100644 libs/ui/elements/src/lib/list/list.component.css delete mode 100644 libs/ui/elements/src/lib/list/list.component.html delete mode 100644 libs/ui/elements/src/lib/list/list.component.spec.ts delete mode 100644 libs/ui/elements/src/lib/list/list.component.ts diff --git a/libs/ui/elements/src/index.ts b/libs/ui/elements/src/index.ts index 2fa6c1da53..f48507d17b 100644 --- a/libs/ui/elements/src/index.ts +++ b/libs/ui/elements/src/index.ts @@ -3,8 +3,6 @@ export * from './lib/avatar/avatar.component' export * from './lib/content-ghost/content-ghost.component' export * from './lib/download-item/download-item.component' export * from './lib/downloads-list/downloads-list.component' -export * from './lib/list/list.component' -export * from './lib/list-item/list-item.component' export * from './lib/image-overlay-preview/image-overlay-preview.component' export * from './lib/link-card/link-card.component' export * from './lib/markdown-editor/markdown-editor.component' diff --git a/libs/ui/elements/src/lib/list-item/list-item.component.css b/libs/ui/elements/src/lib/list-item/list-item.component.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/libs/ui/elements/src/lib/list-item/list-item.component.html b/libs/ui/elements/src/lib/list-item/list-item.component.html deleted file mode 100644 index 500bfe3fc4..0000000000 --- a/libs/ui/elements/src/lib/list-item/list-item.component.html +++ /dev/null @@ -1,18 +0,0 @@ - -
-
- {{ link.description || link.name }} -
-
-
- open_in_new -
-
diff --git a/libs/ui/elements/src/lib/list-item/list-item.component.spec.ts b/libs/ui/elements/src/lib/list-item/list-item.component.spec.ts deleted file mode 100644 index 0c528e3003..0000000000 --- a/libs/ui/elements/src/lib/list-item/list-item.component.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MatIconModule } from '@angular/material/icon' -import { TranslateModule } from '@ngx-translate/core' -import { ListItemComponent } from './list-item.component' -import { ChangeDetectionStrategy } from '@angular/core' - -describe('ListItemComponent', () => { - let component: ListItemComponent - let fixture: ComponentFixture - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ListItemComponent], - imports: [MatIconModule, TranslateModule.forRoot()], - }) - .overrideComponent(ListItemComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() - }) - - beforeEach(() => { - fixture = TestBed.createComponent(ListItemComponent) - 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', () => {}) -}) diff --git a/libs/ui/elements/src/lib/list-item/list-item.component.stories.ts b/libs/ui/elements/src/lib/list-item/list-item.component.stories.ts deleted file mode 100644 index 8fab74d0cb..0000000000 --- a/libs/ui/elements/src/lib/list-item/list-item.component.stories.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - applicationConfig, - componentWrapperDecorator, - Meta, - moduleMetadata, - StoryObj, -} from '@storybook/angular' -import { ListItemComponent } from './list-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/ListItemComponent', - component: ListItemComponent, - 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/list-item/list-item.component.ts b/libs/ui/elements/src/lib/list-item/list-item.component.ts deleted file mode 100644 index df8809136c..0000000000 --- a/libs/ui/elements/src/lib/list-item/list-item.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - Component, - ChangeDetectionStrategy, - Input, - Output, - EventEmitter, -} from '@angular/core' - -@Component({ - selector: 'gn-ui-list-item', - templateUrl: './list-item.component.html', - styleUrls: ['./list-item.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ListItemComponent { - @Input() link: any - @Input() color: string - @Input() format: string - @Input() isFromWfs: boolean - @Output() exportUrl = new EventEmitter() -} diff --git a/libs/ui/elements/src/lib/list/list.component.css b/libs/ui/elements/src/lib/list/list.component.css deleted file mode 100644 index 2a9121aa44..0000000000 --- a/libs/ui/elements/src/lib/list/list.component.css +++ /dev/null @@ -1,32 +0,0 @@ -:host ::ng-deep .mat-mdc-paginator-range-label { - display: none; -} -:host ::ng-deep .mat-mdc-paginator-navigation-previous, -:host ::ng-deep .mat-mdc-paginator-navigation-next { - display: none; -} - -:host ::ng-deep .mat-mdc-paginator-container { - width: 0; - padding: 0; -} - -.carousel-step-dot { - width: 6px; - height: 6px; - border-radius: 6px; - position: relative; -} - -.carousel-step-dot:after { - content: ''; - position: absolute; - left: -4px; - top: -4px; - width: 14px; - height: 14px; -} - -.previous-page-button { - width: 50px; -} diff --git a/libs/ui/elements/src/lib/list/list.component.html b/libs/ui/elements/src/lib/list/list.component.html deleted file mode 100644 index 12335170c2..0000000000 --- a/libs/ui/elements/src/lib/list/list.component.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
-
- -
-
- -
-
-
- - - > diff --git a/libs/ui/elements/src/lib/list/list.component.spec.ts b/libs/ui/elements/src/lib/list/list.component.spec.ts deleted file mode 100644 index b970fa24a8..0000000000 --- a/libs/ui/elements/src/lib/list/list.component.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - DebugElement, - Input, - NO_ERRORS_SCHEMA, -} from '@angular/core' -import { ComponentFixture, TestBed } from '@angular/core/testing' -import { LinkClassifierService } from '@geonetwork-ui/util/shared' -import { TranslateModule } from '@ngx-translate/core' -import { ListComponent } from './list.component' -import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record' - -@Component({ - selector: 'gn-ui-download-item', - template: ``, -}) -class MockDownloadItemComponent { - @Input() link: DatasetDistribution - @Input() color: string - @Input() format: string - @Input() isFromWfs: boolean -} - -describe('ListComponent', () => { - let component: ListComponent - let fixture: ComponentFixture - let de: DebugElement - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [ListComponent, MockDownloadItemComponent], - schemas: [NO_ERRORS_SCHEMA], - providers: [LinkClassifierService], - }) - .overrideComponent(ListComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }) - .compileComponents() - }) - - beforeEach(() => { - fixture = TestBed.createComponent(ListComponent) - component = fixture.componentInstance - component.listItems = [] - de = fixture.debugElement - }) - - it('should create', () => { - fixture.detectChanges() - expect(component).toBeTruthy() - }) -}) diff --git a/libs/ui/elements/src/lib/list/list.component.ts b/libs/ui/elements/src/lib/list/list.component.ts deleted file mode 100644 index 6b7f2e318e..0000000000 --- a/libs/ui/elements/src/lib/list/list.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - Input, - OnChanges, - SimpleChanges, - ViewChild, -} from '@angular/core' -import { TranslateService } from '@ngx-translate/core' -import { DatasetDistribution } from '@geonetwork-ui/common/domain/model/record' -import { MatPaginator, PageEvent } from '@angular/material/paginator' - -@Component({ - selector: 'gn-ui-list', - templateUrl: './list.component.html', - styleUrls: ['./list.component.css'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ListComponent implements OnChanges { - @ViewChild('matPaginator') matPaginator: MatPaginator - - @Input() listItems: any[] - @Input() title = '' - @Input() listPageSize = 1 - - currentPage = 0 - listLength = 1 - listPages = [] - - isFirstPage = true - isLastPage = false - - constructor( - private readonly translateService: TranslateService, - private readonly changeDetector: ChangeDetectorRef - ) {} - - ngOnChanges(changes: SimpleChanges): void { - const listItems = changes['listItems'] - const listItemsValue = listItems.currentValue as any[] - const listHasChanged = listItems.currentValue !== listItems.previousValue - - if (listHasChanged) { - this.listLength = listItemsValue.length - const listPages = Math.ceil(listItemsValue.length / this.listPageSize) - - this.listPages = [] - for (let i = 0; i < listPages; i++) { - this.listPages.push(i) - } - } - this.changeDetector.markForCheck() - } - - /** - * Click on previous arrow - */ - goToPreviousPage() { - if (this.isFirstPage) return - this.matPaginator.previousPage() - this.checkListProperties() - this.changeDetector.markForCheck() - } - - /** - * Click on next arrow - */ - goToNextPage() { - if (this.isLastPage) return - this.matPaginator.nextPage() - this.checkListProperties() - this.changeDetector.markForCheck() - } - - /** - * Click on bottom buttons - * @param newIndex - */ - goToPage(newIndex: number) { - this.currentPage = newIndex - this.matPaginator.pageIndex = newIndex - this.checkListProperties() - this.changeDetector.markForCheck() - } - - /** - * Called internaly by goToNextPage() and goToPreviousPage() - * @param event - */ - pageChanged(event: PageEvent) { - this.currentPage = event.pageIndex - this.checkListProperties() - this.changeDetector.markForCheck() - } - - private checkListProperties() { - this.isFirstPage = this.currentPage === 0 - this.isLastPage = this.currentPage === this.listPages.length - 1 - } - - get filteredListItems(): DatasetDistribution[] { - const startIndex = this.currentPage * this.listPageSize - const endIndex = startIndex + this.listPageSize - - return this.listItems.slice(startIndex, endIndex) - } -} diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index 48cd3dcf80..ff83d14455 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -33,8 +33,6 @@ import { MarkdownParserComponent } from './markdown-parser/markdown-parser.compo 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' -import { ListComponent } from './list/list.component' -import { ListItemComponent } from './list-item/list-item.component' import { MatPaginatorModule } from '@angular/material/paginator' @NgModule({ @@ -60,8 +58,6 @@ import { MatPaginatorModule } from '@angular/material/paginator' ContentGhostComponent, DownloadItemComponent, DownloadsListComponent, - ListComponent, - ListItemComponent, ApiCardComponent, RelatedRecordCardComponent, MetadataContactComponent, @@ -84,8 +80,6 @@ import { MatPaginatorModule } from '@angular/material/paginator' ContentGhostComponent, DownloadItemComponent, DownloadsListComponent, - ListComponent, - ListItemComponent, ApiCardComponent, RelatedRecordCardComponent, MetadataContactComponent, From b6e2babc309ddb51b6319c314d1bb10b16dca2ad Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 9 May 2024 21:45:32 +0200 Subject: [PATCH 12/20] chore: remove unused/redundant code --- libs/feature/record/src/lib/state/mdview.facade.ts | 12 +++--------- libs/ui/elements/src/lib/ui-elements.module.ts | 2 -- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/libs/feature/record/src/lib/state/mdview.facade.ts b/libs/feature/record/src/lib/state/mdview.facade.ts index 2086564c30..37364de54d 100644 --- a/libs/feature/record/src/lib/state/mdview.facade.ts +++ b/libs/feature/record/src/lib/state/mdview.facade.ts @@ -7,11 +7,9 @@ import { LinkClassifierService, LinkUsage } from '@geonetwork-ui/util/shared' import { DatavizConfigurationModel } from '@geonetwork-ui/common/domain/model/dataviz/dataviz-configuration.model' import { CatalogRecord, - DatasetDistribution, UserFeedback, } from '@geonetwork-ui/common/domain/model/record' import { AvatarServiceInterface } from '@geonetwork-ui/api/repository' -import { Observable } from 'rxjs' @Injectable() /** @@ -48,15 +46,11 @@ export class MdViewFacade { error$ = this.store.pipe(select(MdViewSelectors.getMetadataError)) - related$: Observable = this.store.pipe( - select(MdViewSelectors.getRelated) - ) + related$ = this.store.pipe(select(MdViewSelectors.getRelated)) - chartConfig$: Observable = this.store.pipe( - select(MdViewSelectors.getChartConfig) - ) + chartConfig$ = this.store.pipe(select(MdViewSelectors.getChartConfig)) - allLinks$: Observable = this.metadata$.pipe( + allLinks$ = this.metadata$.pipe( map((record) => ('distributions' in record ? record.distributions : [])) ) diff --git a/libs/ui/elements/src/lib/ui-elements.module.ts b/libs/ui/elements/src/lib/ui-elements.module.ts index ff83d14455..58d85ece10 100644 --- a/libs/ui/elements/src/lib/ui-elements.module.ts +++ b/libs/ui/elements/src/lib/ui-elements.module.ts @@ -33,7 +33,6 @@ import { MarkdownParserComponent } from './markdown-parser/markdown-parser.compo 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' -import { MatPaginatorModule } from '@angular/material/paginator' @NgModule({ imports: [ @@ -51,7 +50,6 @@ import { MatPaginatorModule } from '@angular/material/paginator' MarkdownParserComponent, ThumbnailComponent, TimeSincePipe, - MatPaginatorModule, ], declarations: [ MetadataInfoComponent, From cb71222d65dc357b19142871430b98ea78a9d9c2 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 9 May 2024 21:54:48 +0200 Subject: [PATCH 13/20] chore(ui): simplified html of the prev-next-btns component Also do not include it in the ui-inputs module; as a standalone component, it should be imported wherever this is necessary --- apps/datahub/src/app/app.module.ts | 6 +++++- libs/ui/inputs/src/index.ts | 1 + .../previous-next-buttons.component.css | 6 ++++++ .../previous-next-buttons.component.html | 8 -------- libs/ui/inputs/src/lib/ui-inputs.module.ts | 3 --- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/datahub/src/app/app.module.ts b/apps/datahub/src/app/app.module.ts index 2baf778115..e2b035fad1 100644 --- a/apps/datahub/src/app/app.module.ts +++ b/apps/datahub/src/app/app.module.ts @@ -29,7 +29,10 @@ import { THUMBNAIL_PLACEHOLDER, UiElementsModule, } from '@geonetwork-ui/ui/elements' -import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { + PreviousNextButtonsComponent, + UiInputsModule, +} from '@geonetwork-ui/ui/inputs' import { BlockListComponent, CarouselComponent, @@ -158,6 +161,7 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] LinkCardComponent, CarouselComponent, BlockListComponent, + PreviousNextButtonsComponent, ], providers: [ importProvidersFrom(FeatureAuthModule), diff --git a/libs/ui/inputs/src/index.ts b/libs/ui/inputs/src/index.ts index 88f97f9216..1baf8d6f45 100644 --- a/libs/ui/inputs/src/index.ts +++ b/libs/ui/inputs/src/index.ts @@ -19,3 +19,4 @@ export * from './lib/text-area/text-area.component' export * from './lib/text-input/text-input.component' export * from './lib/ui-inputs.module' export * from './lib/viewport-intersector/viewport-intersector.component' +export * from './lib/previous-next-buttons/previous-next-buttons.component' diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css index e69de29bb2..14ad317ff0 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.css @@ -0,0 +1,6 @@ +:host { + --gn-ui-button-rounded: 100%; + --gn-ui-button-width: 8px; + --gn-ui-button-height: 8px; + --gn-ui-button-padding: 12px; +} diff --git a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html index 9203615ca4..d92b9276c0 100644 --- a/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html +++ b/libs/ui/inputs/src/lib/previous-next-buttons/previous-next-buttons.component.html @@ -1,10 +1,6 @@
Date: Mon, 13 May 2024 16:06:32 +0200 Subject: [PATCH 14/20] refactor(record-downloads): catch OGC API error later --- .../record-downloads.component.ts | 22 +++++++++---- .../dataviz/src/lib/service/data.service.ts | 32 ++++++++----------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/apps/datahub/src/app/record/record-downloads/record-downloads.component.ts b/apps/datahub/src/app/record/record-downloads/record-downloads.component.ts index ca0ce42eb1..7f01590ff3 100644 --- a/apps/datahub/src/app/record/record-downloads/record-downloads.component.ts +++ b/apps/datahub/src/app/record/record-downloads/record-downloads.component.ts @@ -56,16 +56,26 @@ export class RecordDownloadsComponent { return combineLatest([ ...(wfsLinks.length > 0 ? wfsLinks.map((link) => - this.dataService.getDownloadLinksFromWfs( - link as DatasetServiceDistribution - ) + this.dataService + .getDownloadLinksFromWfs(link as DatasetServiceDistribution) + .pipe( + catchError((e) => { + this.error = e.message + return [of([] as DatasetDistribution[])] + }) + ) ) : [of([] as DatasetDistribution[])]), ...(ogcLinks.length > 0 ? ogcLinks.map((link) => - this.dataService.getDownloadLinksFromOgcApiFeatures( - link as DatasetServiceDistribution - ) + this.dataService + .getDownloadLinksFromOgcApiFeatures( + link as DatasetServiceDistribution + ) + .catch((e) => { + this.error = e.message + return Promise.resolve([]) + }) ) : [of([] as DatasetDistribution[])]), ]).pipe( diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index 993a142933..1b2299f498 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -156,25 +156,19 @@ export class DataService { async getDownloadLinksFromOgcApiFeatures( ogcApiLink: DatasetServiceDistribution ): Promise { - try { - const collectionInfo = await this.getDownloadUrlsFromOgcApi( - ogcApiLink.url.href - ) - return Object.keys(collectionInfo.bulkDownloadLinks).map( - (downloadLink) => { - return { - ...ogcApiLink, - type: 'download', - url: new URL(collectionInfo.bulkDownloadLinks[downloadLink]), - mimeType: getMimeTypeForFormat( - getFileFormatFromServiceOutput(downloadLink) - ), - } - } - ) - } catch (error) { - return Promise.resolve([]) - } + const collectionInfo = await this.getDownloadUrlsFromOgcApi( + ogcApiLink.url.href + ) + return Object.keys(collectionInfo.bulkDownloadLinks).map((downloadLink) => { + return { + ...ogcApiLink, + type: 'download', + url: new URL(collectionInfo.bulkDownloadLinks[downloadLink]), + mimeType: getMimeTypeForFormat( + getFileFormatFromServiceOutput(downloadLink) + ), + } + }) } async getDownloadUrlsFromOgcApi(url: string): Promise { From 075d1a420357322bb5138980037530399ad8b07b Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Wed, 15 May 2024 23:13:10 +0200 Subject: [PATCH 15/20] feat(ui): keep initial height of the block list when page changes --- .../lib/block-list/block-list.component.html | 7 +++++- .../block-list/block-list.component.spec.ts | 25 ++++++++++++++++++- .../lib/block-list/block-list.component.ts | 14 ++++++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.html b/libs/ui/layout/src/lib/block-list/block-list.component.html index 56223f920f..17e706ddd6 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.html +++ b/libs/ui/layout/src/lib/block-list/block-list.component.html @@ -1,4 +1,9 @@ -
+
`, }) @@ -147,4 +147,27 @@ describe('BlockListComponent', () => { expect(component.isLastPage).toBe(false) }) }) + + describe('set initial height as min height, keeps value when height changes', () => { + beforeEach(() => { + Object.defineProperties(component.blockContainer.nativeElement, { + clientHeight: { + value: 150, + }, + }) + fixture.detectChanges() + component.ngAfterViewInit() + Object.defineProperties(component.blockContainer.nativeElement, { + clientHeight: { + value: 50, + }, + }) + fixture.detectChanges() + }) + it('sets the min height of the container according to its initial content', () => { + expect(component.blockContainer.nativeElement.style.minHeight).toBe( + '150px' + ) + }) + }) }) diff --git a/libs/ui/layout/src/lib/block-list/block-list.component.ts b/libs/ui/layout/src/lib/block-list/block-list.component.ts index a20ae957f0..5a0937c761 100644 --- a/libs/ui/layout/src/lib/block-list/block-list.component.ts +++ b/libs/ui/layout/src/lib/block-list/block-list.component.ts @@ -1,5 +1,5 @@ import { - AfterContentInit, + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -7,6 +7,7 @@ import { ElementRef, Input, QueryList, + ViewChild, } from '@angular/core' import { CommonModule } from '@angular/common' @@ -18,13 +19,16 @@ import { CommonModule } from '@angular/common' standalone: true, imports: [CommonModule], }) -export class BlockListComponent implements AfterContentInit { +export class BlockListComponent implements AfterViewInit { @Input() pageSize = 5 @Input() containerClass = '' @Input() paginationContainerClass = 'w-full bottom-0 top-auto' @ContentChildren('block', { read: ElementRef }) blocks: QueryList< ElementRef > + @ViewChild('blockContainer') blockContainer: ElementRef + + protected minHeight = 0 protected currentPage = 0 protected get pages() { @@ -43,9 +47,13 @@ export class BlockListComponent implements AfterContentInit { constructor(private changeDetector: ChangeDetectorRef) {} - ngAfterContentInit() { + ngAfterViewInit() { this.blocks.changes.subscribe(this.refreshBlocksVisibility) this.refreshBlocksVisibility() + + // we store the first height as the min-height of the list container + this.minHeight = this.blockContainer.nativeElement.clientHeight + this.changeDetector.detectChanges() } protected refreshBlocksVisibility = () => { From 1ef846f6c3b5ead6e51a52298454f31ab4f4f4f1 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 9 May 2024 23:06:22 +0200 Subject: [PATCH 16/20] chore: fix failing tests --- .../record-otherlinks/record-otherlinks.component.spec.ts | 6 +++--- .../elements/src/lib/link-card/link-card.component.spec.ts | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts index 13ad395a38..bc4d48e632 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.spec.ts @@ -1,12 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { Subject } from 'rxjs' +import { BehaviorSubject } from 'rxjs' import { RecordOtherlinksComponent } from './record-otherlinks.component' import { MdViewFacade } from '@geonetwork-ui/feature/record' class MdViewFacadeMock { - otherLinks$ = new Subject() + otherLinks$ = new BehaviorSubject([]) } -describe('DataOtherlinksComponent', () => { +describe('RecordOtherlinksComponent', () => { let component: RecordOtherlinksComponent let fixture: ComponentFixture diff --git a/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts b/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts index 1c84cd4b94..95bcdc690e 100644 --- a/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts +++ b/libs/ui/elements/src/lib/link-card/link-card.component.spec.ts @@ -1,7 +1,5 @@ import { NO_ERRORS_SCHEMA } from '@angular/core' import { ComponentFixture, TestBed } from '@angular/core/testing' -import { MatIconModule } from '@angular/material/icon' -import { TranslateModule } from '@ngx-translate/core' import { LinkCardComponent } from './link-card.component' describe('LinkCardComponent', () => { @@ -10,9 +8,8 @@ describe('LinkCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [LinkCardComponent], schemas: [NO_ERRORS_SCHEMA], - imports: [MatIconModule, TranslateModule.forRoot()], + imports: [LinkCardComponent], }).compileComponents() }) From 0fa453f818e3271020168191cb08fc7bb348e06a Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 16 May 2024 10:06:18 +0200 Subject: [PATCH 17/20] fix(dh): restore overflow-hidden in API form This was hiding the underlying user feebacks form --- .../src/app/record/record-apis/record-apis.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.html b/apps/datahub/src/app/record/record-apis/record-apis.component.html index 61cec4c40e..a4bf38449f 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.html +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.html @@ -28,7 +28,7 @@
Date: Thu, 16 May 2024 11:49:00 +0200 Subject: [PATCH 18/20] e2e: attempt to make the login command more reliable --- tools/e2e/commands.ts | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/tools/e2e/commands.ts b/tools/e2e/commands.ts index b3b5ec3cc6..0c06e446e3 100644 --- a/tools/e2e/commands.ts +++ b/tools/e2e/commands.ts @@ -35,18 +35,33 @@ Cypress.Commands.add( }, }) cy.getCookie('XSRF-TOKEN').then((xsrfTokenCookie) => { - cy.request({ - method: 'POST', - url: '/geonetwork/signin', - body: `username=${username}&password=${password}&_csrf=${xsrfTokenCookie.value}`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - followRedirect: false, - }) + // do the login 2 times because it sometimes doesn't register (?) + for (let i = 0; i < 2; i++) { + cy.request({ + method: 'POST', + url: '/geonetwork/signin', + body: `username=${username}&password=${password}&_csrf=${xsrfTokenCookie.value}`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirect: false, + }) + } + }) + cy.request({ + method: 'GET', + url: '/geonetwork/srv/api/me', + headers: { + Accept: 'application/json', + }, + }).then((response) => { + if (response.status !== 200) { + throw new Error('Could not log in to GeoNetwork API 😢') + } + cy.log('Login to GeoNetwork API successful!') }) - if (redirect) return cy.visit('/') - else return cy.window() + if (redirect) cy.visit('/') + return cy.window() } ) From 55e7abf75d9867fc744a30addff0536126ae84e0 Mon Sep 17 00:00:00 2001 From: Olivia Guyot Date: Thu, 16 May 2024 14:59:07 +0200 Subject: [PATCH 19/20] feat(dh): use ngrxLet to avoid multiple async pipes --- apps/datahub/src/app/app.module.ts | 2 + .../record-otherlinks.component.html | 37 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/apps/datahub/src/app/app.module.ts b/apps/datahub/src/app/app.module.ts index e2b035fad1..4cac62f591 100644 --- a/apps/datahub/src/app/app.module.ts +++ b/apps/datahub/src/app/app.module.ts @@ -91,6 +91,7 @@ import { RecordApisComponent } from './record/record-apis/record-apis.component' import { MatTabsModule } from '@angular/material/tabs' import { UiWidgetsModule } from '@geonetwork-ui/ui/widgets' import { RecordUserFeedbacksComponent } from './record/record-user-feedbacks/record-user-feedbacks.component' +import { LetDirective } from '@ngrx/component' export const metaReducers: MetaReducer[] = !environment.production ? [] : [] @@ -162,6 +163,7 @@ export const metaReducers: MetaReducer[] = !environment.production ? [] : [] CarouselComponent, BlockListComponent, PreviousNextButtonsComponent, + LetDirective, ], providers: [ importProvidersFrom(FeatureAuthModule), diff --git a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html index d6404df489..b97bc8a202 100644 --- a/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html +++ b/apps/datahub/src/app/record/record-otherlinks/record-otherlinks.component.html @@ -12,29 +12,28 @@ (directionButtonClicked)="changeStepOrPage($event)" >
- - + + + + + + + - - - - - From 53b70780af1d79ea9c17bdc7f8ded38adfd748a5 Mon Sep 17 00:00:00 2001 From: Tobias Kohr Date: Fri, 17 May 2024 10:50:18 +0200 Subject: [PATCH 20/20] refactor(data.service): adapt tests to refactoring also add fixture to test that erroneous OGC API does not break download list and seperate generic OGC API error message from WFS messages --- .../record-downloads.component.spec.ts | 11 +++++++-- .../src/lib/service/data.service.spec.ts | 21 +++++++++-------- .../dataviz/src/lib/service/data.service.ts | 20 ++-------------- translations/de.json | 23 ++++++++----------- translations/en.json | 23 ++++++++----------- translations/es.json | 23 ++++++++----------- translations/fr.json | 23 ++++++++----------- translations/it.json | 23 ++++++++----------- translations/nl.json | 23 ++++++++----------- translations/pt.json | 23 ++++++++----------- translations/sk.json | 23 ++++++++----------- 11 files changed, 95 insertions(+), 141 deletions(-) diff --git a/apps/datahub/src/app/record/record-downloads/record-downloads.component.spec.ts b/apps/datahub/src/app/record/record-downloads/record-downloads.component.spec.ts index 176a3c1ea9..9a60784dd3 100644 --- a/apps/datahub/src/app/record/record-downloads/record-downloads.component.spec.ts +++ b/apps/datahub/src/app/record/record-downloads/record-downloads.component.spec.ts @@ -57,8 +57,8 @@ class DataServiceMock { ]) getDownloadLinksFromOgcApiFeatures = jest.fn((link) => link.url.toString().indexOf('error') > -1 - ? throwError(() => new Error('would not fetch links')) - : of([ + ? Promise.reject(new Error('ogc.unreachable.unknown')) + : Promise.resolve([ { ...link, mimeType: 'application/geo+json', @@ -233,6 +233,13 @@ describe('DataDownloadsComponent', () => { type: 'service', accessServiceProtocol: 'ogcFeatures', }, + { + name: 'Some erroneous OGC API service', + description: 'OGC API service', + url: newUrl('https://error.org/collections/airports/items'), + type: 'service', + accessServiceProtocol: 'ogcFeatures', + }, ]) fixture.detectChanges() }) diff --git a/libs/feature/dataviz/src/lib/service/data.service.spec.ts b/libs/feature/dataviz/src/lib/service/data.service.spec.ts index 2748eeac7a..9a367ce61a 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.spec.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.spec.ts @@ -492,15 +492,18 @@ describe('DataService', () => { }) }) describe('calling getDownloadLinksFromOgcApiFeatures() with a erroneous URL', () => { - it('returns empty list of links', async () => { - const url = new URL('http://error.http/ogcapi') - const links = await service.getDownloadLinksFromOgcApiFeatures({ - name: 'mycollection', - url, - type: 'service', - accessServiceProtocol: 'ogcFeatures', - }) - expect(links).toEqual([]) + it('returns an error', async () => { + try { + const url = new URL('http://error.http/ogcapi') + await service.getDownloadLinksFromOgcApiFeatures({ + name: 'mycollection', + url, + type: 'service', + accessServiceProtocol: 'ogcFeatures', + }) + } catch (e) { + expect(e.message).toBe('ogc.unreachable.unknown') + } }) }) }) diff --git a/libs/feature/dataviz/src/lib/service/data.service.ts b/libs/feature/dataviz/src/lib/service/data.service.ts index 1b2299f498..e330b30f1e 100644 --- a/libs/feature/dataviz/src/lib/service/data.service.ts +++ b/libs/feature/dataviz/src/lib/service/data.service.ts @@ -32,6 +32,7 @@ marker('wfs.unreachable.http') marker('wfs.unreachable.unknown') marker('wfs.featuretype.notfound') marker('wfs.geojsongml.notsupported') +marker('ogc.unreachable.unknown') marker('dataset.error.network') marker('dataset.error.http') marker('dataset.error.parse') @@ -178,24 +179,7 @@ export class DataService { return endpoint.getCollectionInfo(collections[0]) }) .catch((error) => { - if (error instanceof Error) { - throw new Error(`wfs.unreachable.unknown`) - } else { - if (error.type === 'network') { - throw new Error(`wfs.unreachable.cors`) - } - if (error.type === 'http') { - throw new Error(`wfs.unreachable.http`) - } - if (error.type === 'parse') { - throw new Error(`wfs.unreachable.parse`) - } - if (error.type === 'unsupportedType') { - throw new Error(`wfs.unreachable.unsupportedType`) - } else { - throw new Error(`wfs.unreachable.unknown`) - } - } + throw new Error(`ogc.unreachable.unknown`) }) } diff --git a/translations/de.json b/translations/de.json index f13017a660..7e9df0c40f 100644 --- a/translations/de.json +++ b/translations/de.json @@ -226,6 +226,7 @@ "multiselect.filter.placeholder": "Suche", "nav.back": "Zurück", "next": "weiter", + "ogc.unreachable.unknown": "Der Dienst konnte nicht erreicht werden", "organisation.filter.placeholder": "Ergebnisse filtern", "organisation.sort.sortBy": "Sortieren nach:", "organisations.hits.found": "{hits, plural, =0{Keine Organisation gefunden} other{{hits} von {total} Organisationen angezeigt}}", @@ -290,14 +291,6 @@ "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", @@ -312,6 +305,14 @@ "record.metadata.updateFrequency": "Aktualisierungsfrequenz der Daten", "record.metadata.updatedOn": "Letzte Aktualisierung der Dateninformationen", "record.metadata.usage": "Nutzung und Einschränkungen", + "record.metadata.userFeedbacks": "", + "record.metadata.userFeedbacks.anonymousUser": "", + "record.metadata.userFeedbacks.newAnswer.buttonTitle": "", + "record.metadata.userFeedbacks.newAnswer.placeholder": "", + "record.metadata.userFeedbacks.newComment.placeholder": "", + "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "", + "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "", + "record.metadata.userFeedbacks.sortSelector.label": "", "record.more.details": "Weitere Details", "record.tab.chart": "Diagramm", "record.tab.data": "Tabelle", @@ -371,12 +372,6 @@ "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 ae41dc7bd5..a126cde62b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -226,6 +226,7 @@ "multiselect.filter.placeholder": "Search", "nav.back": "Back", "next": "next", + "ogc.unreachable.unknown": "The service could not be reached", "organisation.filter.placeholder": "Filter results", "organisation.sort.sortBy": "Sort by:", "organisations.hits.found": "{hits, plural, =0{No organizations found} other{{hits} out of {total} organizations shown}}", @@ -290,14 +291,6 @@ "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", @@ -312,6 +305,14 @@ "record.metadata.updateFrequency": "Data Update Frequency", "record.metadata.updatedOn": "Last Data Information Update", "record.metadata.usage": "License and Conditions", + "record.metadata.userFeedbacks": "Questions / Answers", + "record.metadata.userFeedbacks.anonymousUser": "In order to leave a comment, please log in.", + "record.metadata.userFeedbacks.newAnswer.buttonTitle": "Publish", + "record.metadata.userFeedbacks.newAnswer.placeholder": "Answer...", + "record.metadata.userFeedbacks.newComment.placeholder": "Write your comment here...", + "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "Newest comments first", + "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "Oldest comments first", + "record.metadata.userFeedbacks.sortSelector.label": "Sort by ...", "record.more.details": "Read more", "record.tab.chart": "Chart", "record.tab.data": "Table", @@ -371,12 +372,6 @@ "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 80f3137515..26c5e3ccb8 100644 --- a/translations/es.json +++ b/translations/es.json @@ -226,6 +226,7 @@ "multiselect.filter.placeholder": "", "nav.back": "", "next": "", + "ogc.unreachable.unknown": "", "organisation.filter.placeholder": "", "organisation.sort.sortBy": "", "organisations.hits.found": "", @@ -290,14 +291,6 @@ "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": "", @@ -312,6 +305,14 @@ "record.metadata.updateFrequency": "", "record.metadata.updatedOn": "", "record.metadata.usage": "", + "record.metadata.userFeedbacks": "", + "record.metadata.userFeedbacks.anonymousUser": "", + "record.metadata.userFeedbacks.newAnswer.buttonTitle": "", + "record.metadata.userFeedbacks.newAnswer.placeholder": "", + "record.metadata.userFeedbacks.newComment.placeholder": "", + "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "", + "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "", + "record.metadata.userFeedbacks.sortSelector.label": "", "record.more.details": "", "record.tab.chart": "", "record.tab.data": "", @@ -371,12 +372,6 @@ "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 f90d19689a..a78c3cfc22 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -226,6 +226,7 @@ "multiselect.filter.placeholder": "Rechercher", "nav.back": "Retour", "next": "suivant", + "ogc.unreachable.unknown": "Le service n'est pas accessible", "organisation.filter.placeholder": "Filtrer les résultats", "organisation.sort.sortBy": "Trier par :", "organisations.hits.found": "{hits, plural, =0{Aucune organisation trouvé} one{1 organisation sur {total} affichée} other{{hits} organisations sur {total} affichées}}", @@ -290,14 +291,6 @@ "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", @@ -312,6 +305,14 @@ "record.metadata.updateFrequency": "Fréquence de mise à jour des données", "record.metadata.updatedOn": "Dernière mise à jour des informations sur les données", "record.metadata.usage": "Licences et conditions d'utilisation", + "record.metadata.userFeedbacks": "Questions / Réponses", + "record.metadata.userFeedbacks.anonymousUser": "Pour rédiger un commentaire, veuillez vous identifier.", + "record.metadata.userFeedbacks.newAnswer.buttonTitle": "Publier", + "record.metadata.userFeedbacks.newAnswer.placeholder": "Répondre...", + "record.metadata.userFeedbacks.newComment.placeholder": "Rédiger votre commentaire ici...", + "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.sortSelector.label": "Trier par ...", "record.more.details": "Détails", "record.tab.chart": "Graphique", "record.tab.data": "Tableau", @@ -371,12 +372,6 @@ "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 a7b41ab08b..c8aacdc045 100644 --- a/translations/it.json +++ b/translations/it.json @@ -226,6 +226,7 @@ "multiselect.filter.placeholder": "Cerca", "nav.back": "Indietro", "next": "successivo", + "ogc.unreachable.unknown": "Il servizio non è accessibile", "organisation.filter.placeholder": "Filtra i risultati", "organisation.sort.sortBy": "Ordina per:", "organisations.hits.found": "{hits, plural, =0{Nessuna organizzazione trovata} one{1 organizzazione su {total} visualizzata} other{{hits} organizzazioni su {total} visualizzate}}", @@ -290,14 +291,6 @@ "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", @@ -312,6 +305,14 @@ "record.metadata.updateFrequency": "Frequenza di aggiornamento dei dati", "record.metadata.updatedOn": "Ultimo aggiornamento delle informazioni sui dati", "record.metadata.usage": "Licenze e limiti di utilizzo", + "record.metadata.userFeedbacks": "", + "record.metadata.userFeedbacks.anonymousUser": "", + "record.metadata.userFeedbacks.newAnswer.buttonTitle": "", + "record.metadata.userFeedbacks.newAnswer.placeholder": "", + "record.metadata.userFeedbacks.newComment.placeholder": "", + "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "", + "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "", + "record.metadata.userFeedbacks.sortSelector.label": "", "record.more.details": "Dettagli", "record.tab.chart": "Grafico", "record.tab.data": "Tabella", @@ -371,12 +372,6 @@ "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 9d4f7930af..89616d1e96 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -226,6 +226,7 @@ "multiselect.filter.placeholder": "", "nav.back": "", "next": "", + "ogc.unreachable.unknown": "", "organisation.filter.placeholder": "", "organisation.sort.sortBy": "", "organisations.hits.found": "", @@ -290,14 +291,6 @@ "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": "", @@ -312,6 +305,14 @@ "record.metadata.updateFrequency": "", "record.metadata.updatedOn": "", "record.metadata.usage": "", + "record.metadata.userFeedbacks": "", + "record.metadata.userFeedbacks.anonymousUser": "", + "record.metadata.userFeedbacks.newAnswer.buttonTitle": "", + "record.metadata.userFeedbacks.newAnswer.placeholder": "", + "record.metadata.userFeedbacks.newComment.placeholder": "", + "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "", + "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "", + "record.metadata.userFeedbacks.sortSelector.label": "", "record.more.details": "", "record.tab.chart": "", "record.tab.data": "", @@ -371,12 +372,6 @@ "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 c753cb1f9d..031e933c7a 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -226,6 +226,7 @@ "multiselect.filter.placeholder": "", "nav.back": "", "next": "", + "ogc.unreachable.unknown": "", "organisation.filter.placeholder": "", "organisation.sort.sortBy": "", "organisations.hits.found": "", @@ -290,14 +291,6 @@ "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": "", @@ -312,6 +305,14 @@ "record.metadata.updateFrequency": "", "record.metadata.updatedOn": "", "record.metadata.usage": "", + "record.metadata.userFeedbacks": "", + "record.metadata.userFeedbacks.anonymousUser": "", + "record.metadata.userFeedbacks.newAnswer.buttonTitle": "", + "record.metadata.userFeedbacks.newAnswer.placeholder": "", + "record.metadata.userFeedbacks.newComment.placeholder": "", + "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "", + "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "", + "record.metadata.userFeedbacks.sortSelector.label": "", "record.more.details": "", "record.tab.chart": "", "record.tab.data": "", @@ -371,12 +372,6 @@ "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 efe0fa9685..006938fead 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -226,6 +226,7 @@ "multiselect.filter.placeholder": "Hľadať", "nav.back": "Späť", "next": "Ďalej", + "ogc.unreachable.unknown": "So službou sa nedalo spojiť", "organisation.filter.placeholder": "Filtrovať výsledky", "organisation.sort.sortBy": "Zoradiť podľa:", "organisations.hits.found": "{hits, plural, =0{Žiadna organizácia nenájdená} other{{hits} celkovo {total} organizácií nájdených}}", @@ -290,14 +291,6 @@ "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": "", @@ -312,6 +305,14 @@ "record.metadata.updateFrequency": "Frekvencia aktualizácie metadát", "record.metadata.updatedOn": "Posledná aktualizácia metadát", "record.metadata.usage": "Použitie a obmedzenia", + "record.metadata.userFeedbacks": "", + "record.metadata.userFeedbacks.anonymousUser": "", + "record.metadata.userFeedbacks.newAnswer.buttonTitle": "", + "record.metadata.userFeedbacks.newAnswer.placeholder": "", + "record.metadata.userFeedbacks.newComment.placeholder": "", + "record.metadata.userFeedbacks.sortSelector.choices.newestFirst": "", + "record.metadata.userFeedbacks.sortSelector.choices.oldestFirst": "", + "record.metadata.userFeedbacks.sortSelector.label": "", "record.more.details": "Čítať viac", "record.tab.chart": "Graf", "record.tab.data": "Tabuľka", @@ -371,12 +372,6 @@ "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",