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 3b47fc48a5..90cdaae443 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 @@ -26,7 +26,8 @@ export class ApiCardComponent implements OnInit, OnChanges { ngOnInit() { this.displayApiFormButton = - this.link.accessServiceProtocol === 'ogcFeatures' ? true : false + this.link.accessServiceProtocol === 'ogcFeatures' || + this.link.accessServiceProtocol === 'wfs' } ngOnChanges(changes: SimpleChanges) { 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 index 2c9bd0881f..81e72b4daa 100644 --- 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 @@ -37,15 +37,31 @@ -
-

record.metadata.api.form.offset

- - +
+

+ record.metadata.api.form.offset +

+
+ + +
+ + warning + +
+

record.metadata.api.form.type

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 53b718f21c..65b193e11b 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 @@ -29,6 +29,30 @@ jest.mock('@camptocamp/ogc-client', () => ({ }) } }, + WfsEndpoint: class { + constructor(private url) {} + async isReady() { + return Promise.resolve(true) + } + getFeatureUrl(featureType, options) { + return `${this.url}?type=${featureType}&options=${JSON.stringify( + options + )}` + } + getServiceInfo() { + return Promise.resolve({ + outputFormats: [ + 'application/geo+json', + 'application/json', + 'text/csv', + 'application/json', + ], + }) + } + supportsStartIndex() { + return true + } + }, })) describe('RecordApFormComponent', () => { @@ -122,6 +146,29 @@ describe('RecordApFormComponent', () => { ]) }) }) + + describe('When panel is opened and accessServiceProtocol is wfs', () => { + beforeEach(() => { + component.apiLink = { + ...mockDatasetServiceDistribution, + accessServiceProtocol: 'wfs', + } + fixture.detectChanges() + }) + + it('should set the links and initial values correctly', async () => { + expect(component.apiBaseUrl).toBe('https://api.example.com/data') + expect(component.accessServiceProtocol).toBe('wfs') + expect(component.offset$.getValue()).toBe('') + expect(component.limit$.getValue()).toBe('-1') + expect(component.format$.getValue()).toBe('json') + const url = await firstValueFrom(component.apiQueryUrl$) + expect(url).toBe( + 'https://api.example.com/data?type=undefined&options={"outputFormat":"json","startIndex":0}' + ) + }) + }) + describe('When apiLink input is undefined', () => { it('should not call parseOutputFormats()', () => { const spy = jest.spyOn(component, 'parseOutputFormats') 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 2377c20495..15178539a1 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 @@ -1,8 +1,11 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { OgcApiEndpoint } from '@camptocamp/ogc-client' -import { DatasetServiceDistribution } from '@geonetwork-ui/common/domain/model/record' +import { OgcApiEndpoint, WfsEndpoint } from '@camptocamp/ogc-client' +import { + DatasetServiceDistribution, + ServiceProtocol, +} from '@geonetwork-ui/common/domain/model/record' import { mimeTypeToFormat } from '@geonetwork-ui/util/shared' -import { BehaviorSubject, combineLatest, map } from 'rxjs' +import { BehaviorSubject, combineLatest, map, switchMap } from 'rxjs' const DEFAULT_PARAMS = { OFFSET: '', @@ -18,19 +21,26 @@ const DEFAULT_PARAMS = { export class RecordApiFormComponent { @Input() set apiLink(value: DatasetServiceDistribution) { this.outputFormats = [{ value: 'json', label: 'JSON' }] + this.accessServiceProtocol = value ? value.accessServiceProtocol : undefined + this.apiFeatureType = value ? value.name : undefined if (value) { this.apiBaseUrl = value.url.href this.parseOutputFormats() } this.resetUrl() } + offset$ = new BehaviorSubject('') limit$ = new BehaviorSubject('') format$ = new BehaviorSubject('') apiBaseUrl: string + apiFeatureType: string + supportOffset = true + accessServiceProtocol: ServiceProtocol | undefined outputFormats = [{ value: 'json', label: 'JSON' }] + apiQueryUrl$ = combineLatest([this.offset$, this.limit$, this.format$]).pipe( - map(([offset, limit, format]) => { + switchMap(async ([offset, limit, format]) => { let outputUrl if (this.apiBaseUrl) { const url = new URL(this.apiBaseUrl) @@ -44,6 +54,20 @@ export class RecordApiFormComponent { } outputUrl = url.toString() } + + if (this.accessServiceProtocol === 'wfs') { + const wfsEndpoint = new WfsEndpoint(this.apiBaseUrl) + if (await wfsEndpoint.isReady()) { + const options = { + outputFormat: format, + startIndex: Number(offset), + } + if (limit !== '-1') { + options['maxFeatures'] = Number(limit) + } + outputUrl = wfsEndpoint.getFeatureUrl(this.apiFeatureType, options) + } + } return outputUrl }) ) @@ -80,32 +104,49 @@ export class RecordApiFormComponent { ? this.apiBaseUrl.slice(0, -1) : this.apiBaseUrl - this.getOutputFormats(apiUrl).then((outputFormats) => { - const formatsList = outputFormats.itemFormats.map((format) => { - const normalizedFormat = mimeTypeToFormat(format) - if (normalizedFormat) { - return { - label: normalizedFormat?.toUpperCase(), - value: normalizedFormat, - } + this.getOutputFormats(apiUrl, this.accessServiceProtocol).then( + (outputFormats) => { + let formatsList = [] + if ('itemFormats' in outputFormats) { + formatsList = this.mapFormats(outputFormats.itemFormats) + } else if ('outputFormats' in outputFormats) { + formatsList = this.mapFormats(outputFormats.outputFormats) } - return null - }) - this.outputFormats = this.outputFormats.concat( - formatsList.filter(Boolean) - ) - this.outputFormats = this.outputFormats - .filter( - (format, index, self) => - index === self.findIndex((t) => t.value === format.value) + this.outputFormats = this.outputFormats.concat( + formatsList.filter(Boolean) ) - .sort((a, b) => a.label.localeCompare(b.label)) + this.outputFormats = this.outputFormats + .filter( + (format, index, self) => + index === self.findIndex((t) => t.value === format.value) + ) + .sort((a, b) => a.label.localeCompare(b.label)) + } + ) + } + + mapFormats(formats: any[]) { + return formats.map((format) => { + const normalizedFormat = mimeTypeToFormat(format) + if (normalizedFormat) { + return { + label: normalizedFormat.toUpperCase(), + value: normalizedFormat, + } + } + return null }) } - async getOutputFormats(url) { - const endpoint = await new OgcApiEndpoint(url) - const firstCollection = (await endpoint.featureCollections)[0] - return endpoint.getCollectionInfo(firstCollection) + async getOutputFormats(url: string, accessServiceProtocol: string) { + if (accessServiceProtocol === 'wfs') { + const endpoint = await new WfsEndpoint(url).isReady() + this.supportOffset = endpoint.supportsStartIndex() + return endpoint.getServiceInfo() + } else { + const endpoint = await new OgcApiEndpoint(url) + const firstCollection = (await endpoint.featureCollections)[0] + return endpoint.getCollectionInfo(firstCollection) + } } } diff --git a/libs/ui/inputs/src/lib/text-input/text-input.component.html b/libs/ui/inputs/src/lib/text-input/text-input.component.html index df92d6bbb3..4aace20e25 100644 --- a/libs/ui/inputs/src/lib/text-input/text-input.component.html +++ b/libs/ui/inputs/src/lib/text-input/text-input.component.html @@ -8,4 +8,5 @@ [placeholder]="hint" [attr.aria-label]="hint" [attr.required]="required || null" + [disabled]="disabled" /> diff --git a/libs/ui/inputs/src/lib/text-input/text-input.component.stories.ts b/libs/ui/inputs/src/lib/text-input/text-input.component.stories.ts index c615b67a76..c7c51d4283 100644 --- a/libs/ui/inputs/src/lib/text-input/text-input.component.stories.ts +++ b/libs/ui/inputs/src/lib/text-input/text-input.component.stories.ts @@ -12,6 +12,7 @@ export const Primary: StoryObj = { value: '', hint: 'Put something here!', required: false, + disabled: false, }, argTypes: { valueChange: { diff --git a/libs/ui/inputs/src/lib/text-input/text-input.component.ts b/libs/ui/inputs/src/lib/text-input/text-input.component.ts index cb75c50e76..03560b15fa 100644 --- a/libs/ui/inputs/src/lib/text-input/text-input.component.ts +++ b/libs/ui/inputs/src/lib/text-input/text-input.component.ts @@ -29,6 +29,7 @@ export class TextInputComponent implements AfterViewInit { @Input() extraClass = '' @Input() hint: string @Input() required = false + @Input() disabled: boolean rawChange = new Subject() @Output() valueChange = this.rawChange.pipe(distinctUntilChanged()) @ViewChild('input') input diff --git a/package-lock.json b/package-lock.json index 39816ce869..1fb333e55d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@angular/router": "16.1.7", "@bartholomej/ngx-translate-extract": "^8.0.2", "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", - "@camptocamp/ogc-client": "^1.1.1-dev.a0aadb6", + "@camptocamp/ogc-client": "^1.1.1-dev.ddbb5b0", "@geospatial-sdk/geocoding": "^0.0.5-alpha.2", "@ltd/j-toml": "~1.35.2", "@messageformat/core": "^3.0.1", @@ -3652,9 +3652,9 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@camptocamp/ogc-client": { - "version": "1.1.1-dev.a0aadb6", - "resolved": "https://registry.npmjs.org/@camptocamp/ogc-client/-/ogc-client-1.1.1-dev.a0aadb6.tgz", - "integrity": "sha512-wQsCe7GHYoUEr71vO8ov74GoNrHbCS+cfvimJ055OTHcEOIjrFC3Hs88P0WmgXmdUuGoFZLpJ6EF1eeYisGL+Q==", + "version": "1.1.1-dev.ddbb5b0", + "resolved": "https://registry.npmjs.org/@camptocamp/ogc-client/-/ogc-client-1.1.1-dev.ddbb5b0.tgz", + "integrity": "sha512-XBunVVWEh/e/CXpAx9QxysT2v0uNDOKXz4VsFINt5uFXZimovl6wKzgI1pt7lU6gxLuAeWXHnwnLm3XfUHmKag==", "dependencies": { "@rgrove/parse-xml": "^4.1.0" }, @@ -17891,9 +17891,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/ejs": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz", - "integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dependencies": { "jake": "^10.8.5" }, diff --git a/package.json b/package.json index e01ac3607e..3355674655 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@angular/router": "16.1.7", "@bartholomej/ngx-translate-extract": "^8.0.2", "@biesbjerg/ngx-translate-extract-marker": "^1.0.0", - "@camptocamp/ogc-client": "^1.1.1-dev.a0aadb6", + "@camptocamp/ogc-client": "^1.1.1-dev.ddbb5b0", "@geospatial-sdk/geocoding": "^0.0.5-alpha.2", "@ltd/j-toml": "~1.35.2", "@messageformat/core": "^3.0.1",