From 65c2806160c16957fb6fec0bebdf94be1d8763e8 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 21 Dec 2023 18:21:06 +0100 Subject: [PATCH 01/11] feat(dh): adapt api card to handle form --- .../src/lib/api-card/api-card.component.html | 38 ++++++++++++++++--- .../lib/api-card/api-card.component.spec.ts | 33 ++++++++++------ .../src/lib/api-card/api-card.component.ts | 36 +++++++++++++++++- 3 files changed, 88 insertions(+), 19 deletions(-) diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.html b/libs/ui/elements/src/lib/api-card/api-card.component.html index ba834b4d0d..676de845c2 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.html +++ b/libs/ui/elements/src/lib/api-card/api-card.component.html @@ -1,23 +1,51 @@
{{ link.name || link.description }}
{{ link.accessServiceProtocol.toUpperCase() }}{{ link.accessServiceProtocol }} +
diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts b/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts index 1261e98035..2da1df059b 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts +++ b/libs/ui/elements/src/lib/api-card/api-card.component.spec.ts @@ -1,18 +1,16 @@ -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 { ApiCardComponent } from './api-card.component' +import { TranslateModule } from '@ngx-translate/core' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' describe('ApiCardComponent', () => { let component: ApiCardComponent let fixture: ComponentFixture - + let openRecordApiFormEmit beforeEach(async () => { await TestBed.configureTestingModule({ - schemas: [NO_ERRORS_SCHEMA], declarations: [ApiCardComponent], - imports: [MatIconModule, TranslateModule.forRoot()], + imports: [TranslateModule.forRoot()], }).compileComponents() }) @@ -20,16 +18,27 @@ describe('ApiCardComponent', () => { fixture = TestBed.createComponent(ApiCardComponent) component = fixture.componentInstance component.link = { - name: 'Allroads', - description: 'A file that contains all roads', - url: new URL('https://roads.com/wfs'), - type: 'service', - accessServiceProtocol: 'wfs', - } + accessServiceProtocol: 'ogcFeatures', + } as DatasetServiceDistribution + openRecordApiFormEmit = component.openRecordApiForm + jest.resetAllMocks() + jest.spyOn(openRecordApiFormEmit, 'emit') fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + + it('should initialize custom property based on accessServiceProtocol', () => { + component.ngOnInit() + expect(component.displayApiFormButton).toBe(true) + }) + + it('should toggle currentlyActive and emit openRecordApiForm event', () => { + component.openRecordApiFormPanel() + + expect(component.currentlyActive).toBe(true) + expect(openRecordApiFormEmit.emit).toHaveBeenCalled() + }) }) diff --git a/libs/ui/elements/src/lib/api-card/api-card.component.ts b/libs/ui/elements/src/lib/api-card/api-card.component.ts index 9f9888d3a0..c10ad4f600 100644 --- a/libs/ui/elements/src/lib/api-card/api-card.component.ts +++ b/libs/ui/elements/src/lib/api-card/api-card.component.ts @@ -1,5 +1,14 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from '@angular/core' @Component({ selector: 'gn-ui-api-card', @@ -7,6 +16,29 @@ import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/r styleUrls: ['./api-card.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ApiCardComponent { +export class ApiCardComponent implements OnInit, OnChanges { @Input() link: DatasetServiceDistribution + @Input() currentLink: DatasetServiceDistribution + displayApiFormButton: boolean + currentlyActive = false + @Output() openRecordApiForm: EventEmitter = + new EventEmitter() + + ngOnInit() { + this.displayApiFormButton = + this.link.accessServiceProtocol === 'ogcFeatures' ? true : false + } + + ngOnChanges(changes: SimpleChanges) { + this.currentlyActive = + changes.currentLink.currentValue === this.link ? true : false + } + + openRecordApiFormPanel() { + if (this.displayApiFormButton) { + this.currentlyActive = !this.currentlyActive + console.log(this.currentlyActive) + this.openRecordApiForm.emit(this.currentlyActive ? this.link : undefined) + } + } } From a15caad6d0d34474f93cc4a674702c5ba0bc3c43 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 21 Dec 2023 18:21:33 +0100 Subject: [PATCH 02/11] feat(dh): create record api form --- .../record-api-form.component.css | 22 +++++ .../record-api-form.component.html | 75 ++++++++++++++++ .../record-api-form.component.spec.ts | 90 +++++++++++++++++++ .../record-api-form.component.ts | 65 ++++++++++++++ 4 files changed, 252 insertions(+) create mode 100644 libs/ui/elements/src/lib/record-api-form/record-api-form.component.css create mode 100644 libs/ui/elements/src/lib/record-api-form/record-api-form.component.html create mode 100644 libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts create mode 100644 libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.css b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.css new file mode 100644 index 0000000000..0aa4ba7d0b --- /dev/null +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.css @@ -0,0 +1,22 @@ +:host ::ng-deep input { + color: black; + opacity: 1; +} + +:host ::ng-deep gn-ui-copy-text-button input[type='text'] { + color: black; + background-color: white; +} + +:host ::ng-deep gn-ui-copy-text-button button, +host ::ng-deep gn-ui-copy-text-button button:hover { + background-color: var(--color-secondary) !important; +} + +:host ::ng-deep gn-ui-copy-text-button button mat-icon { + color: white !important; + opacity: 1 !important; +} +:host ::ng-deep gn-ui-copy-text-button button:hover mat-icon { + color: lightgrey !important; +} diff --git a/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html new file mode 100644 index 0000000000..b6328f5891 --- /dev/null +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.html @@ -0,0 +1,75 @@ +
+
+
+
+ record.metadata.api.form.create +
+ +
+
+
+

record.metadata.api.form.limit

+
+ + +
+ + record.metadata.api.form.limit.all +
+
+
+
+

record.metadata.api.form.offset

+ + +
+
+

record.metadata.api.form.type

+ +
+
+
+
+
+ record.metadata.api.form.customUrl +
+
+ +
+
+
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 new file mode 100644 index 0000000000..d21e700497 --- /dev/null +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.spec.ts @@ -0,0 +1,90 @@ +import { TestBed, ComponentFixture } from '@angular/core/testing' +import { RecordApiFormComponent } from './record-api-form.component' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' +import { firstValueFrom } from 'rxjs' +import { UiInputsModule } from '@geonetwork-ui/ui/inputs' +import { TranslateModule } from '@ngx-translate/core' + +const mockDatasetServiceDistribution: DatasetServiceDistribution = { + url: new URL('https://api.example.com/data'), + type: 'service', + accessServiceProtocol: 'ogcFeatures', +} + +describe('RecordApFormComponent', () => { + let component: RecordApiFormComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RecordApiFormComponent], + imports: [UiInputsModule, TranslateModule.forRoot()], + }).compileComponents() + + fixture = TestBed.createComponent(RecordApiFormComponent) + component = fixture.componentInstance + component.apiLink = mockDatasetServiceDistribution + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + describe('When panel is opened', () => { + it('should set the links and initial values correctly', async () => { + expect(component.apiBaseUrl).toBe('https://api.example.com/data') + expect(component.offset$.getValue()).toBe('') + expect(component.limit$.getValue()).toBe('') + expect(component.format$.getValue()).toBe('json') + const url = await firstValueFrom(component.apiQueryUrl$) + expect(url).toBe('https://api.example.com/data?f=json') + }) + }) + describe('When URL params are changed', () => { + it('should update query URL correctly when setting offset, limit, and format', async () => { + const mockOffset = '10' + const mockLimit = '20' + const mockFormat = 'json' + component.setOffset(mockOffset) + component.setLimit(mockLimit) + component.setFormat(mockFormat) + const url = await firstValueFrom(component.apiQueryUrl$) + expect(url).toBe( + `https://api.example.com/data?offset=${mockOffset}&limit=${mockLimit}&f=${mockFormat}` + ) + }) + it('should remove the param in url if value is null', async () => { + const mockOffset = null + const mockLimit = '20' + const mockFormat = 'json' + component.setOffset(mockOffset) + component.setLimit(mockLimit) + component.setFormat(mockFormat) + const url = await firstValueFrom(component.apiQueryUrl$) + expect(url).toBe( + `https://api.example.com/data?limit=${mockLimit}&f=${mockFormat}` + ) + }) + it('should remove the param in url if value is zero', async () => { + const mockOffset = '10' + const mockLimit = '0' + const mockFormat = 'json' + component.setOffset(mockOffset) + component.setLimit(mockLimit) + component.setFormat(mockFormat) + const url = await firstValueFrom(component.apiQueryUrl$) + expect(url).toBe( + `https://api.example.com/data?offset=${mockOffset}&f=${mockFormat}` + ) + }) + }) + + describe('#resetUrl', () => { + it('should reset URL to default parameters', () => { + component.resetUrl() + expect(component.offset$.getValue()).toBe('') + expect(component.limit$.getValue()).toBe('') + expect(component.format$.getValue()).toBe('json') + }) + }) +}) 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 new file mode 100644 index 0000000000..497eddab1a --- /dev/null +++ b/libs/ui/elements/src/lib/record-api-form/record-api-form.component.ts @@ -0,0 +1,65 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' +import { BehaviorSubject, combineLatest, map } from 'rxjs' + +const DEFAULT_PARAMS = { + OFFSET: '', + LIMIT: '', + FORMAT: 'json', +} +@Component({ + selector: 'gn-ui-record-api-form', + templateUrl: './record-api-form.component.html', + styleUrls: ['./record-api-form.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RecordApiFormComponent { + @Input() set apiLink(value: DatasetServiceDistribution) { + this.apiBaseUrl = value ? value.url.href : undefined + this.resetUrl() + } + offset$ = new BehaviorSubject('') + limit$ = new BehaviorSubject('') + format$ = new BehaviorSubject('') + apiBaseUrl: string + formatsList = [ + { label: 'JSON', value: 'json' }, + { label: 'CSV', value: 'csv' }, + ] + apiQueryUrl$ = combineLatest([this.offset$, this.limit$, this.format$]).pipe( + map(([offset, limit, format]) => { + let outputUrl + if (this.apiBaseUrl) { + const url = new URL(this.apiBaseUrl) + const params = { offset: offset, limit: limit, f: format } + for (const [key, value] of Object.entries(params)) { + if (value && value !== '0') { + url.searchParams.set(key, value) + } else { + url.searchParams.delete(key) + } + } + outputUrl = url.toString() + } + return outputUrl + }) + ) + + setOffset(value: string) { + this.offset$.next(value) + } + + setLimit(value: string) { + this.limit$.next(value) + } + + setFormat(value: string) { + this.format$.next(value) + } + + resetUrl() { + this.offset$.next(DEFAULT_PARAMS.OFFSET) + this.limit$.next(DEFAULT_PARAMS.LIMIT) + this.format$.next(DEFAULT_PARAMS.FORMAT) + } +} From e7d57bbc721450e82b27cd41216f4e677298b6f4 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 21 Dec 2023 18:21:56 +0100 Subject: [PATCH 03/11] feat(dh/me): create date range picker --- .../date-range-picker.component.css | 3 ++ .../date-range-picker.component.html | 24 ++++++++++++ .../date-range-picker.component.spec.ts | 38 +++++++++++++++++++ .../date-range-picker.component.ts | 20 ++++++++++ 4 files changed, 85 insertions(+) create mode 100644 libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.css create mode 100644 libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.html create mode 100644 libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts create mode 100644 libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.css b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.css new file mode 100644 index 0000000000..58dae5804f --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.css @@ -0,0 +1,3 @@ +mat-datepicker-toggle { + @apply text-primary; +} diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.html b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.html new file mode 100644 index 0000000000..dc7eb44a9e --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.html @@ -0,0 +1,24 @@ +
+
+ + + + +
+ + calendar_today + + +
diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts new file mode 100644 index 0000000000..d3fe405cef --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { MatDatepickerInputEvent } from '@angular/material/datepicker' +import { DateRangePickerComponent } from './date-range-picker.component' + +describe('DateRangePickerComponent', () => { + let component: DateRangePickerComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DateRangePickerComponent], + }).compileComponents() + + fixture = TestBed.createComponent(DateRangePickerComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should set start date on startDateSelected', () => { + const event = { + value: new Date('2023-01-01'), + } as MatDatepickerInputEvent + component.startDateSelected(event) + expect(component.startDate).toEqual(new Date('2023-01-01')) + }) + + it('should set end date on endDateSelected', () => { + const event = { + value: new Date('2023-01-31'), + } as MatDatepickerInputEvent + component.endDateSelected(event) + expect(component.endDate).toEqual(new Date('2023-01-31')) + }) +}) diff --git a/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts new file mode 100644 index 0000000000..a4593cdf75 --- /dev/null +++ b/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core' +import { MatDatepickerInputEvent } from '@angular/material/datepicker' + +@Component({ + selector: 'gn-ui-date-range-picker', + templateUrl: './date-range-picker.component.html', + styleUrls: ['./date-range-picker.component.css'], +}) +export class DateRangePickerComponent { + startDate: Date + endDate: Date + + startDateSelected(event: MatDatepickerInputEvent) { + this.startDate = event.value + } + + endDateSelected(event: MatDatepickerInputEvent) { + this.endDate = event.value + } +} From 26ef852048d6a3193dbed8ffda6a4f832b54f052 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 21 Dec 2023 18:22:16 +0100 Subject: [PATCH 04/11] feat(dh): implement api form --- .../record-apis/record-apis.component.css | 3 + .../record-apis/record-apis.component.html | 88 ++++++++++++++----- .../record-apis/record-apis.component.spec.ts | 35 ++++++-- .../record-apis/record-apis.component.ts | 26 +++++- 4 files changed, 121 insertions(+), 31 deletions(-) diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.css b/apps/datahub/src/app/record/record-apis/record-apis.component.css index e69de29bb2..5b19499cd1 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.css +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.css @@ -0,0 +1,3 @@ +.tab-header-label { + @apply uppercase text-sm text-primary opacity-75 hover:text-primary-darker; +} 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 b7b623d5e7..329f7b34b2 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,25 +1,65 @@ -

- record.metadata.api -

- - +

- - + record.metadata.api +

+ + + + + +
+
+
+
+

+ record.metadata.api.form.title +

+ +
+ +
+
+
diff --git a/apps/datahub/src/app/record/record-apis/record-apis.component.spec.ts b/apps/datahub/src/app/record/record-apis/record-apis.component.spec.ts index 948b6b2fc0..6ff7242cb3 100644 --- a/apps/datahub/src/app/record/record-apis/record-apis.component.spec.ts +++ b/apps/datahub/src/app/record/record-apis/record-apis.component.spec.ts @@ -1,19 +1,29 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' -import { Subject } from 'rxjs' import { RecordApisComponent } from './record-apis.component' +import { TranslateModule } from '@ngx-translate/core' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' +import { BehaviorSubject, firstValueFrom } from 'rxjs' class MdViewFacadeMock { - apiLinks$ = new Subject() + selectedApiLink$ = new BehaviorSubject([]) } -describe('DataApisComponent', () => { +const serviceDistributionMock = { + type: 'service', + url: new URL('http://myogcapifeatures.test'), + accessServiceProtocol: 'ogcFeatures', +} as DatasetServiceDistribution + +describe('RecordApisComponent', () => { let component: RecordApisComponent let fixture: ComponentFixture + let facade beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [RecordApisComponent], + imports: [TranslateModule.forRoot()], providers: [ { provide: MdViewFacade, @@ -21,15 +31,30 @@ describe('DataApisComponent', () => { }, ], }).compileComponents() - }) - beforeEach(() => { fixture = TestBed.createComponent(RecordApisComponent) component = fixture.componentInstance + facade = TestBed.inject(MdViewFacade) fixture.detectChanges() }) it('should create', () => { expect(component).toBeTruthy() }) + + describe('#openRecordApiForm', () => { + beforeEach(() => { + component.openRecordApiForm(serviceDistributionMock) + }) + it('should update selectedApiLink$', async () => { + expect(component.selectedApiLink).toEqual(serviceDistributionMock) + }) + }) + + describe('#closeRecordApiForm', () => { + it('should pass undefined to selectedApiLink$', async () => { + component.closeRecordApiForm() + expect(component.selectedApiLink).toBeUndefined() + }) + }) }) 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 8bcfd1ce86..050fc87475 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 @@ -1,4 +1,5 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core' +import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core' +import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' import { MdViewFacade } from '@geonetwork-ui/feature/record' @Component({ @@ -7,6 +8,27 @@ import { MdViewFacade } from '@geonetwork-ui/feature/record' styleUrls: ['./record-apis.component.css'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class RecordApisComponent { +export class RecordApisComponent implements OnInit { + maxHeight = '' + selectedApiLink: DatasetServiceDistribution constructor(public facade: MdViewFacade) {} + + ngOnInit(): void { + this.maxHeight = this.setMaxHeight(undefined) + this.selectedApiLink = undefined + } + + openRecordApiForm(link: DatasetServiceDistribution) { + this.selectedApiLink = link + this.maxHeight = this.setMaxHeight(link) + } + + closeRecordApiForm() { + this.selectedApiLink = undefined + this.maxHeight = this.setMaxHeight(undefined) + } + + setMaxHeight(link: DatasetServiceDistribution) { + return `${link === undefined ? '0' : '428'}px` + } } From 73998bfb4483e6d910cc85b77ba052ac46cb5902 Mon Sep 17 00:00:00 2001 From: Camille Moinier Date: Thu, 21 Dec 2023 18:23:04 +0100 Subject: [PATCH 05/11] feat(dh): change record-api implementation --- .../record-metadata.component.html | 9 +- .../record-metadata.component.spec.ts | 86 +++++++++++++++---- 2 files changed, 76 insertions(+), 19 deletions(-) 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 f7f020a96c..e332f92203 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 @@ -114,13 +114,16 @@ -