diff --git a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html index 14db76bfad..fdb8ca9455 100644 --- a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html +++ b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.html @@ -17,6 +17,7 @@

class="flex flex-row gap-[8px]" [(ngModel)]="service.accessServiceProtocol" [disabled]="disabled" + (change)="resetLayersSuggestion()" >
- + + + +

+ editor.record.form.field.onlineResource.edit.identifier.error +

+ - + + - editor.record.form.field.onlineResource.edit.identifier.submit + editor.record.form.field.onlineResource.edit.identifier.submit
diff --git a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts index f32acb6d3b..4dfdb67ac4 100644 --- a/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts +++ b/libs/feature/editor/src/lib/components/online-service-resource-input/online-service-resource-input.component.ts @@ -1,10 +1,12 @@ import { CommonModule } from '@angular/common' import { ChangeDetectionStrategy, + ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, + OnInit, Output, } from '@angular/core' import { FormsModule } from '@angular/forms' @@ -17,6 +19,8 @@ import { } from '@geonetwork-ui/common/domain/model/record' import { ButtonComponent, + DropdownChoice, + DropdownSelectorComponent, TextInputComponent, UrlInputComponent, } from '@geonetwork-ui/ui/inputs' @@ -27,6 +31,7 @@ import { provideNgIconsConfig, } from '@ng-icons/core' import { iconoirCloudUpload } from '@ng-icons/iconoir' +import { getLayers } from '@geonetwork-ui/util/shared' @Component({ selector: 'gn-ui-online-service-resource-input', @@ -35,6 +40,7 @@ import { iconoirCloudUpload } from '@ng-icons/iconoir' changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [ + DropdownSelectorComponent, ButtonComponent, CommonModule, FormsModule, @@ -52,18 +58,21 @@ import { iconoirCloudUpload } from '@ng-icons/iconoir' }), ], }) -export class OnlineServiceResourceInputComponent implements OnChanges { - @Input() service: Omit +export class OnlineServiceResourceInputComponent implements OnChanges, OnInit { + @Input() service: DatasetServiceDistribution @Input() protocolHint?: string @Input() disabled? = false + @Input() modifyMode? = false @Output() urlChange: EventEmitter = new EventEmitter() @Output() identifierSubmit: EventEmitter<{ url: string identifier: string }> = new EventEmitter() + errorMessage = false selectedProtocol: ServiceProtocol url: string + layers: DropdownChoice[] | undefined = undefined protocolOptions: { label: string @@ -99,6 +108,14 @@ export class OnlineServiceResourceInputComponent implements OnChanges { }, ] + constructor(private cdr: ChangeDetectorRef) {} + + get activeLayerSuggestion() { + return !['wps', 'GPFDL', 'esriRest', 'other'].includes( + this.service.accessServiceProtocol + ) + } + ngOnChanges() { this.selectedProtocol = this.protocolOptions.find( @@ -106,8 +123,50 @@ export class OnlineServiceResourceInputComponent implements OnChanges { )?.value ?? 'other' } - handleUrlChange(url: string) { + ngOnInit() { + if (this.service.url) { + this.url = this.service.url.toString() + } + } + + handleUrlValueChange(url: string) { + this.url = url + this.service.url = new URL(url) + this.resetLayersSuggestion() + this.urlChange.emit(this.url) + } + + async handleUploadClick(url: string) { this.url = url + + try { + const layers = await getLayers(url, this.service.accessServiceProtocol) + this.layers = layers.map((l) => { + return { + label: l.title ? `${l.title} ${l.name ? `(${l.name})` : ''}` : l.name, + value: l.name || l.title, + } + }) + + if (this.layers.length === 0) { + throw new Error('No layers found') + } + } catch (e) { + this.errorMessage = true + this.layers = undefined + } + + this.cdr.detectChanges() + } + + handleSelectValue(val: string) { + this.service.identifierInService = val + } + + resetLayersSuggestion() { + this.errorMessage = false + this.layers = undefined + this.service.identifierInService = null } submitIdentifier(identifier: string) { diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html index 6598d5dd0d..6424b9b8e5 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.html @@ -21,7 +21,6 @@ @@ -55,19 +54,22 @@

[(value)]="onlineResource.description" > - + - - + + + + diff --git a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts index a33209e5e5..7b3307f9b9 100644 --- a/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts +++ b/libs/feature/editor/src/lib/components/record-form/form-field/form-field-online-resources/form-field-online-resources.component.ts @@ -92,11 +92,12 @@ export class FormFieldOnlineResourcesComponent { notLinkResources: OnlineNotLinkResource[] = [] uploadProgress = undefined uploadSubscription: Subscription = null - newService = { + newService = { type: 'service', accessServiceProtocol: 'ogcFeatures', identifierInService: '', - } as Omit + url: undefined, + } protected MAX_UPLOAD_SIZE_MB = MAX_UPLOAD_SIZE_MB diff --git a/libs/ui/inputs/src/lib/url-input/url-input.component.stories.ts b/libs/ui/inputs/src/lib/url-input/url-input.component.stories.ts index 1a676d4f9b..5a14245b14 100644 --- a/libs/ui/inputs/src/lib/url-input/url-input.component.stories.ts +++ b/libs/ui/inputs/src/lib/url-input/url-input.component.stories.ts @@ -1,10 +1,35 @@ -import { Meta, StoryObj } from '@storybook/angular' +import { + applicationConfig, + Meta, + moduleMetadata, + StoryObj, +} from '@storybook/angular' +import { + NgIconComponent, + provideIcons, + provideNgIconsConfig, +} from '@ng-icons/core' +import { matStar } from '@ng-icons/material-icons/baseline' import { UrlInputComponent } from './url-input.component' export default { title: 'Inputs/UrlInputComponent', component: UrlInputComponent, - decorators: [], + decorators: [ + moduleMetadata({ + imports: [NgIconComponent], + }), + applicationConfig({ + providers: [ + provideIcons({ + matStar, + }), + provideNgIconsConfig({ + size: '0.9em', + }), + ], + }), + ], argTypes: { valueChange: { action: 'valueChange', @@ -56,3 +81,20 @@ export const WithoutUploadButton: StoryObj = { `, }), } + +export const WithCustomValidateButton: StoryObj = { + args: { + value: null, + disabled: false, + placeholder: 'https://mysite.org/file', + showValidateButton: true, + }, + render: (args) => ({ + props: args, + template: ` + + + `, + }), +} diff --git a/libs/util/shared/src/lib/links/link-utils.spec.ts b/libs/util/shared/src/lib/links/link-utils.spec.ts index 6a276cef8b..b7ada94c93 100644 --- a/libs/util/shared/src/lib/links/link-utils.spec.ts +++ b/libs/util/shared/src/lib/links/link-utils.spec.ts @@ -5,12 +5,102 @@ import { getBadgeColor, getFileFormat, getFileFormatFromServiceOutput, + getLayers, getLinkLabel, getLinkPriority, mimeTypeToFormat, } from './link-utils' import { DatasetDownloadDistribution } from '@geonetwork-ui/common/domain/model/record' +jest.mock('@camptocamp/ogc-client', () => ({ + WfsEndpoint: class { + constructor(private url) {} + isReady() { + return Promise.resolve(this) + } + getFeatureTypes() { + return [ + { + name: 'ft1', + title: 'Feature Type 1', + }, + { + name: 'ft2', + title: 'Feature Type 2', + }, + { + name: 'ft3', + title: 'Feature Type 3', + }, + ] + } + }, + OgcApiEndpoint: class { + constructor(private url) {} + get allCollections() { + return [ + { + name: 'ogc-collection-1', + title: 'Ogc Collection 1', + }, + { + name: 'ogc-collection-2', + title: 'Ogc Collection 2', + }, + ] + } + }, + WmsEndpoint: class { + constructor(private url) {} + isReady() { + return Promise.resolve(this) + } + getLayers() { + return [ + { + name: 'wms-layer-1', + title: 'WMS layer 1', + abstract: 'WMS layer 1', + children: [ + { + name: 'wms-layer-1-1', + title: 'WMS layer 1 - 1', + abstract: 'WMS layer 1 - 1', + }, + ], + }, + { + name: 'wms-layer-2', + title: 'WMS layer 2', + abstract: 'WMS layer 2', + }, + ] + } + }, + WmtsEndpoint: class { + constructor(private url) {} + isReady() { + return Promise.resolve(this) + } + getLayers() { + return [ + { + name: 'wmts-layer-1', + title: 'WMTS layer 1', + }, + { + name: 'wmts-layer-2', + title: 'WMTS layer 2', + }, + { + name: 'wmts-layer-3', + title: 'WMTS layer 3', + }, + ] + } + }, +})) + describe('link utils', () => { describe('#getFileFormat', () => { describe('for a csv FILE link', () => { @@ -335,4 +425,86 @@ describe('link utils', () => { ).toEqual('Cities (geojson)') }) }) + + describe('#getLayers', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should return OGC Features layers', async () => { + const layers = await getLayers('https://example.com', 'ogcFeatures') + expect(layers).toEqual([ + { + name: 'ogc-collection-1', + title: 'Ogc Collection 1', + }, + { + name: 'ogc-collection-2', + title: 'Ogc Collection 2', + }, + ]) + }) + + it('should return WFS feature types', async () => { + const layers = await getLayers('https://example.com', 'wfs') + expect(layers).toEqual([ + { + name: 'ft1', + title: 'Feature Type 1', + }, + { + name: 'ft2', + title: 'Feature Type 2', + }, + { + name: 'ft3', + title: 'Feature Type 3', + }, + ]) + }) + + it('should return flattened WMS layers (filtered)', async () => { + const layers = await getLayers('https://example.com', 'wms') + expect(layers).toEqual([ + { + name: 'wms-layer-1', + title: 'WMS layer 1', + abstract: 'WMS layer 1', + }, + { + name: 'wms-layer-1-1', + title: 'WMS layer 1 - 1', + abstract: 'WMS layer 1 - 1', + }, + { + name: 'wms-layer-2', + title: 'WMS layer 2', + abstract: 'WMS layer 2', + }, + ]) + }) + + it('should return WMTS layers', async () => { + const layers = await getLayers('https://example.com', 'wmts') + expect(layers).toEqual([ + { + name: 'wmts-layer-1', + title: 'WMTS layer 1', + }, + { + name: 'wmts-layer-2', + title: 'WMTS layer 2', + }, + { + name: 'wmts-layer-3', + title: 'WMTS layer 3', + }, + ]) + }) + + it('should return undefined for an unknown serviceProtocol', async () => { + const layers = await getLayers('https://example.com', 'unknown' as any) + expect(layers).toBeUndefined() + }) + }) }) diff --git a/libs/util/shared/src/lib/links/link-utils.ts b/libs/util/shared/src/lib/links/link-utils.ts index 329e40b882..75bdd312a5 100644 --- a/libs/util/shared/src/lib/links/link-utils.ts +++ b/libs/util/shared/src/lib/links/link-utils.ts @@ -1,5 +1,14 @@ import { marker } from '@biesbjerg/ngx-translate-extract-marker' -import { DatasetOnlineResource } from '@geonetwork-ui/common/domain/model/record' +import { + DatasetOnlineResource, + ServiceProtocol, +} from '@geonetwork-ui/common/domain/model/record' +import { + OgcApiEndpoint, + WfsEndpoint, + WmsEndpoint, + WmtsEndpoint, +} from '@camptocamp/ogc-client' marker('downloads.wfs.featuretype.not.found') @@ -242,6 +251,47 @@ export function getLinkLabel(link: DatasetOnlineResource): string { return format ? `${label} (${format})` : label } +export async function getLayers(url: string, serviceProtocol: ServiceProtocol) { + switch (serviceProtocol) { + case 'ogcFeatures': { + const layers = await new OgcApiEndpoint(url).allCollections + return layers + } + case 'wfs': { + const endpointWfs = new WfsEndpoint(url) + await endpointWfs.isReady() + return endpointWfs.getFeatureTypes() + } + case 'wms': { + const endpointWms = new WmsEndpoint(url) + await endpointWms.isReady() + return endpointWms + .getLayers() + .flatMap(wmsLayerFlatten) + .filter((l) => l.name) + } + case 'wmts': { + const endpointWmts = new WmtsEndpoint(url) + await endpointWmts.isReady() + return endpointWmts.getLayers() + } + default: + return undefined + } +} + +function wmsLayerFlatten(layerFull) { + const layer = { + title: layerFull.title, + name: layerFull.name, + abstract: layerFull.abstract, + } + + return 'children' in layerFull && Array.isArray(layerFull.children) + ? [layer, ...layerFull.children.flatMap(wmsLayerFlatten)] + : [layer] +} + export function getMimeTypeForFormat(format: FileFormat): string | null { return format in FORMATS ? FORMATS[format.toLowerCase()].mimeTypes[0] : null } diff --git a/translations/de.json b/translations/de.json index 850c3ab37b..fc63b61ca2 100644 --- a/translations/de.json +++ b/translations/de.json @@ -235,8 +235,10 @@ "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", "editor.record.form.field.onlineResource.edit.protocol": "", + "editor.record.form.field.onlineResource.edit.identifier.error": "Schichten konnten nicht vom Protokoll abgerufen werden", "editor.record.form.field.onlineResource.edit.identifier.placeholder": "Ebenenname", "editor.record.form.field.onlineResource.edit.identifier.placeholder.wps": "Prozessname", + "editor.record.form.field.onlineResource.edit.identifier.select.label": "Wählen Sie eine Ebene", "editor.record.form.field.onlineResource.edit.identifier.submit": "Link zum Dienst", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", diff --git a/translations/en.json b/translations/en.json index 1767f8a995..9236932a6c 100644 --- a/translations/en.json +++ b/translations/en.json @@ -235,8 +235,10 @@ "editor.record.form.field.onlineResource.dialogTitle": "Modify the dataset preview", "editor.record.form.field.onlineResource.edit.description": "Description", "editor.record.form.field.onlineResource.edit.protocol": "Protocol", + "editor.record.form.field.onlineResource.edit.identifier.error": "Unable to retrieve layers from protocol", "editor.record.form.field.onlineResource.edit.identifier.placeholder": "Layer name", "editor.record.form.field.onlineResource.edit.identifier.placeholder.wps": "Process name", + "editor.record.form.field.onlineResource.edit.identifier.select.label": "Select a layer", "editor.record.form.field.onlineResource.edit.identifier.submit": "Link to the service", "editor.record.form.field.onlineResource.edit.title": "Title", "editor.record.form.field.onlineResource.fileSize": "{sizeMB}MB", diff --git a/translations/es.json b/translations/es.json index c794b247a2..48807a14a5 100644 --- a/translations/es.json +++ b/translations/es.json @@ -235,8 +235,10 @@ "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", "editor.record.form.field.onlineResource.edit.protocol": "", + "editor.record.form.field.onlineResource.edit.identifier.error": "No se pueden recuperar las capas del protocolo", "editor.record.form.field.onlineResource.edit.identifier.placeholder": "Nombre de la capa", "editor.record.form.field.onlineResource.edit.identifier.placeholder.wps": "Nombre del proceso", + "editor.record.form.field.onlineResource.edit.identifier.select.label": "Selecciona una capa", "editor.record.form.field.onlineResource.edit.identifier.submit": "Enlace al servicio", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", diff --git a/translations/fr.json b/translations/fr.json index 75c5c6b72d..f3a0400234 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -235,8 +235,10 @@ "editor.record.form.field.onlineResource.dialogTitle": "Modifier l'aperçu du jeu de données", "editor.record.form.field.onlineResource.edit.description": "Description", "editor.record.form.field.onlineResource.edit.protocol": "Protocole", + "editor.record.form.field.onlineResource.edit.identifier.error": "Impossible de récupérer les couches depuis le protocole", "editor.record.form.field.onlineResource.edit.identifier.placeholder": "Nom de la couche", "editor.record.form.field.onlineResource.edit.identifier.placeholder.wps": "Nom du processus", + "editor.record.form.field.onlineResource.edit.identifier.select.label": "Sélectionner une couche", "editor.record.form.field.onlineResource.edit.identifier.submit": "Lier le service", "editor.record.form.field.onlineResource.edit.title": "Titre", "editor.record.form.field.onlineResource.fileSize": "{sizeMB} Mo", diff --git a/translations/it.json b/translations/it.json index 45b1925cb4..9ac12ef466 100644 --- a/translations/it.json +++ b/translations/it.json @@ -229,8 +229,10 @@ "editor.record.form.field.keywords": "Parole chiave", "editor.record.form.field.legalConstraints": "Vincolo legale", "editor.record.form.field.license": "Licenza", + "editor.record.form.field.onlineResource.edit.identifier.error": "Impossibile recuperare i livelli dal protocollo", "editor.record.form.field.onlineResource.edit.identifier.placeholder": "Nome del livello", "editor.record.form.field.onlineResource.edit.identifier.placeholder.wps": "Nome del processo", + "editor.record.form.field.onlineResource.edit.identifier.select.label": "Seleziona un livello", "editor.record.form.field.onlineResource.edit.identifier.submit": "Collegare il servizio", "editor.record.form.field.title.placeholder": "Inserisci un titolo", "editor.record.form.field.onlineLinkResources": "Risorse allegate", diff --git a/translations/nl.json b/translations/nl.json index 177194f459..9f939209d5 100644 --- a/translations/nl.json +++ b/translations/nl.json @@ -235,8 +235,10 @@ "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", "editor.record.form.field.onlineResource.edit.protocol": "", + "editor.record.form.field.onlineResource.edit.identifier.error": "Kan lagen niet ophalen van het protocol", "editor.record.form.field.onlineResource.edit.identifier.placeholder": "Laagnaam", "editor.record.form.field.onlineResource.edit.identifier.placeholder.wps": "Procesnaam", + "editor.record.form.field.onlineResource.edit.identifier.select.label": "Selecteer een laag", "editor.record.form.field.onlineResource.edit.identifier.submit": "Link naar de dienst", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", diff --git a/translations/pt.json b/translations/pt.json index 4f029e84c5..18a074e814 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -235,8 +235,10 @@ "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", "editor.record.form.field.onlineResource.edit.protocol": "", + "editor.record.form.field.onlineResource.edit.identifier.error": "Não é possível recuperar camadas do protocolo", "editor.record.form.field.onlineResource.edit.identifier.placeholder": "Nome da camada", "editor.record.form.field.onlineResource.edit.identifier.placeholder.wps": "Nome do processo", + "editor.record.form.field.onlineResource.edit.identifier.select.label": "Selecione uma camada", "editor.record.form.field.onlineResource.edit.identifier.submit": "Link para o serviço", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "", diff --git a/translations/sk.json b/translations/sk.json index 6d5ce62093..ff7443ac9f 100644 --- a/translations/sk.json +++ b/translations/sk.json @@ -235,8 +235,10 @@ "editor.record.form.field.onlineResource.dialogTitle": "", "editor.record.form.field.onlineResource.edit.description": "", "editor.record.form.field.onlineResource.edit.protocol": "", + "editor.record.form.field.onlineResource.edit.identifier.error": "Nie je možné načítať vrstvy z protokolu", "editor.record.form.field.onlineResource.edit.identifier.placeholder": "Názov vrstvy", "editor.record.form.field.onlineResource.edit.identifier.placeholder.wps": "Názov procesu", + "editor.record.form.field.onlineResource.edit.identifier.select.label": "Vyberte vrstvu", "editor.record.form.field.onlineResource.edit.identifier.submit": "Odkaz na službu", "editor.record.form.field.onlineResource.edit.title": "", "editor.record.form.field.onlineResource.fileSize": "",