geonetwork-ui 2.11.0-dev.bc1a6d4e2 → 2.11.0-dev.bc1acb627

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1446 -1300
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +170 -100
  4. package/index.d.ts.map +1 -1
  5. package/package.json +6 -5
  6. package/src/libs/api/metadata-converter/src/lib/dcat-ap/dcat-ap.converter.ts +2 -0
  7. package/src/libs/api/metadata-converter/src/lib/fixtures/mel.records.dataset+featurecatalog.ts +317 -0
  8. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +35 -8
  9. package/src/libs/api/metadata-converter/src/lib/gn4/types/metadata.model.ts +19 -0
  10. package/src/libs/api/metadata-converter/src/lib/iso19115-3/iso19115-3.converter.ts +2 -0
  11. package/src/libs/api/metadata-converter/src/lib/iso19115-3/read-parts.ts +98 -0
  12. package/src/libs/api/metadata-converter/src/lib/iso19139/iso19139.converter.ts +7 -0
  13. package/src/libs/api/metadata-converter/src/lib/iso19139/read-parts.ts +2 -1
  14. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +90 -31
  15. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +5 -34
  16. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +6 -13
  17. package/src/libs/common/domain/src/lib/model/search/aggregation.model.ts +2 -0
  18. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +12 -2
  19. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +1 -1
  20. package/src/libs/common/fixtures/src/lib/records.fixtures.ts +42 -62
  21. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +32 -19
  22. package/src/libs/feature/map/src/index.ts +0 -2
  23. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  24. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
  25. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +3 -1
  26. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -1
  27. package/src/libs/feature/search/src/index.ts +2 -0
  28. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +8 -1
  29. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +26 -4
  30. package/src/libs/feature/search/src/lib/geocoding/geocoding.service.ts +105 -0
  31. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +18 -0
  32. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +114 -0
  33. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +2 -0
  34. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  35. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +62 -4
  36. package/src/libs/feature/search/src/lib/utils/service/fields.ts +67 -9
  37. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.html +2 -2
  38. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.ts +2 -2
  39. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  40. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css +0 -4
  41. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +12 -3
  42. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +11 -3
  43. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  44. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  45. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  46. package/src/libs/ui/inputs/src/lib/search-feature-catalog/search-feature-catalog.component.ts +2 -1
  47. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +8 -2
  48. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +34 -5
  49. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  50. package/src/libs/util/app-config/src/lib/app-config.ts +34 -0
  51. package/src/libs/util/app-config/src/lib/fixtures.ts +37 -1
  52. package/src/libs/util/app-config/src/lib/model.ts +24 -1
  53. package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
  54. package/src/libs/util/shared/src/lib/utils/geojson.ts +16 -0
  55. package/translations/de.json +10 -2
  56. package/translations/en.json +12 -4
  57. package/translations/es.json +10 -2
  58. package/translations/fr.json +17 -9
  59. package/translations/it.json +10 -2
  60. package/translations/nl.json +10 -2
  61. package/translations/pt.json +10 -2
  62. package/translations/sk.json +10 -2
  63. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.css +0 -0
  64. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  65. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  66. package/src/libs/feature/map/src/lib/geocoding.service.ts +0 -57
  67. package/src/libs/feature/map/src/lib/style/index.ts +0 -2
  68. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  69. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
@@ -11,6 +11,7 @@ import {
11
11
  OnInit,
12
12
  Output,
13
13
  SimpleChanges,
14
+ TemplateRef,
14
15
  ViewChild,
15
16
  inject,
16
17
  } from '@angular/core'
@@ -97,6 +98,13 @@ export class AutocompleteComponent
97
98
  @ViewChild(MatAutocompleteTrigger) triggerRef: MatAutocompleteTrigger
98
99
  @ViewChild(MatAutocomplete) autocomplete: MatAutocomplete
99
100
  @ViewChild('searchInput') inputRef: ElementRef<HTMLInputElement>
101
+ /**
102
+ * Custom template to render each suggestion. Takes precedence over
103
+ * `displayWithFn` when set.
104
+ */
105
+ @Input() displayWithTemplate?: TemplateRef<{
106
+ $implicit: AutocompleteItem
107
+ }>
100
108
 
101
109
  searching: boolean
102
110
  control = new UntypedFormControl()
@@ -126,13 +134,13 @@ export class AutocompleteComponent
126
134
  getExtraClass(): string {
127
135
  if (this.allowSubmit) {
128
136
  if (this.enterButton) {
129
- return 'border rounded-lg absolute w-8 h-8 right-[calc(var(--icon-width)+var(--icon-padding))] inset-y-[--icon-padding]'
137
+ return 'border rounded-lg absolute w-8 h-8 right-[calc(var(--icon-width)+var(--icon-padding))] top-1/2 -translate-y-1/2'
130
138
  } else {
131
- return 'border rounded-lg absolute w-8 h-8 right-[calc(var(--icon-width)+0.25*var(--icon-width))] inset-y-[calc(0.25*var(--icon-width))]'
139
+ return 'border rounded-lg absolute w-8 h-8 right-[calc(var(--icon-width)+0.25*var(--icon-width))] top-1/2 -translate-y-1/2'
132
140
  }
133
141
  } else {
134
142
  if (!this.enterButton) {
135
- return 'border rounded-lg absolute w-8 h-8 right-2 inset-y-2'
143
+ return 'border rounded-lg absolute w-8 h-8 right-2 top-1/2 -translate-y-1/2'
136
144
  }
137
145
  }
138
146
  return 'border rounded-lg absolute w-8 h-8'
@@ -20,10 +20,13 @@
20
20
  </div>
21
21
  }
22
22
  </div>
23
- <button class="h-6 w-6" data-test="dropdown-clear">
23
+ <button
24
+ class="h-6 w-6 flex items-center justify-center"
25
+ data-test="dropdown-clear"
26
+ >
24
27
  @if (hasSelectedChoices && !overlayOpen) {
25
28
  <ng-icon
26
- class="shrink-0 opacity-40 mr-1.5 hover:opacity-80 transition-colors clear-btn"
29
+ class="shrink-0 opacity-40 hover:opacity-80 transition-colors clear-btn"
27
30
  (click)="clearSelection($event)"
28
31
  name="matClose"
29
32
  ></ng-icon>
@@ -61,13 +64,13 @@
61
64
  #overlayContainer
62
65
  >
63
66
  <div
64
- class="border border-gray-300 rounded mb-2 mx-2 min-h-[44px] flex flex-row gap-[2px] flex-wrap p-2 focus-within:rounded focus-within:border-2 focus-within:border-primary"
67
+ class="border border-gray-300 rounded mb-2 mx-2 min-h-[44px] flex flex-row gap-1 flex-wrap p-2 focus-within:rounded focus-within:border-2 focus-within:border-primary"
65
68
  >
66
69
  @for (selected of selectedChoices; track selected) {
67
70
  <button
68
71
  type="button"
69
72
  [title]="selected.label"
70
- class="max-w-full bg-main text-white rounded pr-[7px] flex gap-1 items-center opacity-70 hover:opacity-100 focus:opacity-100 transition-opacity mb-1"
73
+ class="max-w-full bg-main text-white rounded pr-[7px] flex gap-1 items-center opacity-70 hover:opacity-100 focus:opacity-100 transition-opacity"
71
74
  (click)="select(selected, false)"
72
75
  >
73
76
  <div class="text-sm truncate leading-[26px] px-2">
@@ -24,7 +24,7 @@ import {
24
24
  propagateToDocumentOnly,
25
25
  } from '../../../../../../libs/util/shared/src'
26
26
  import { ButtonComponent } from '../button/button.component'
27
- import { NgIcon, provideIcons } from '@ng-icons/core'
27
+ import { NgIcon, provideIcons, provideNgIconsConfig } from '@ng-icons/core'
28
28
  import { FormsModule } from '@angular/forms'
29
29
  import { TranslatePipe } from '@ngx-translate/core'
30
30
 
@@ -46,19 +46,22 @@ import {
46
46
  matExpandMore,
47
47
  matExpandLess,
48
48
  }),
49
+ provideNgIconsConfig({
50
+ size: '1.5rem',
51
+ }),
49
52
  ],
50
53
  standalone: true,
51
54
  })
52
- export class DropdownMultiselectComponent {
55
+ export class DropdownMultiselectComponent<T = unknown> {
53
56
  private scrollStrategies = inject(ScrollStrategyOptions)
54
57
 
55
58
  @Input() title: string
56
- @Input() choices: Choice[]
57
- @Input() selected: unknown[] = []
59
+ @Input() choices: Choice<T>[]
60
+ @Input() selected: T[] = []
58
61
  @Input() allowSearch = true
59
62
  @Input() maxRows: number
60
63
  @Input() searchInputValue = ''
61
- @Output() selectValues = new EventEmitter<unknown[]>()
64
+ @Output() selectValues = new EventEmitter<T[]>()
62
65
  @ViewChild('overlayOrigin') overlayOrigin: CdkOverlayOrigin
63
66
  @ViewChild(CdkConnectedOverlay) overlay: CdkConnectedOverlay
64
67
  @ViewChild('overlayContainer', { read: ElementRef })
@@ -115,7 +118,7 @@ export class DropdownMultiselectComponent {
115
118
 
116
119
  private setFocus() {
117
120
  setTimeout(() => {
118
- this.searchFieldInput.nativeElement.focus()
121
+ this.searchFieldInput?.nativeElement.focus()
119
122
  }, 0)
120
123
  }
121
124
 
@@ -194,18 +197,18 @@ export class DropdownMultiselectComponent {
194
197
  this.checkboxes.get(newIndex).nativeElement.focus()
195
198
  }
196
199
 
197
- isSelected(choice: Choice) {
200
+ isSelected(choice: Choice<T>) {
198
201
  return this.selected.indexOf(choice.value) > -1
199
202
  }
200
203
 
201
- select(choice: Choice, selected: boolean) {
204
+ select(choice: Choice<T>, selected: boolean) {
202
205
  this.selected = selected
203
206
  ? [...this.selected.filter((v) => v !== choice.value), choice.value]
204
207
  : this.selected.filter((v) => v !== choice.value)
205
208
  this.selectValues.emit(this.selected)
206
209
  }
207
210
 
208
- toggle(choice: Choice) {
211
+ toggle(choice: Choice<T>) {
209
212
  this.select(choice, !this.isSelected(choice))
210
213
  }
211
214
 
@@ -1,4 +1,4 @@
1
- export interface Choice {
2
- value: unknown
1
+ export interface Choice<T = unknown> {
2
+ value: T
3
3
  label: string
4
4
  }
@@ -56,7 +56,8 @@ export class SearchFeatureCatalogComponent {
56
56
  const fuzzyFilter = createFuzzyFilter(searchTerm)
57
57
  return (
58
58
  fuzzyFilter(featureType.name) ||
59
- (featureType.definition && fuzzyFilter(featureType.definition))
59
+ (featureType.description &&
60
+ fuzzyFilter(featureType.description))
60
61
  )
61
62
  }
62
63
  )
@@ -58,7 +58,12 @@
58
58
  [class]="
59
59
  (hasSelection
60
60
  ? 'border-b-[1px] border-gray-400 pb-[20px]'
61
- : 'pb-[8px]') + ' text-black'
61
+ : 'pb-[12px]') + ' text-black flex flex-col gap-[8px]'
62
+ "
63
+ style="
64
+ --gn-ui-text-input-rounded: 8px;
65
+ --gn-ui-text-input-border-size: 1px;
66
+ --gn-ui-text-input-padding: 6px;
62
67
  "
63
68
  >
64
69
  <gn-ui-drag-and-drop-file-input
@@ -68,12 +73,13 @@
68
73
  [showFileName]="false"
69
74
  textClass="text-black truncate"
70
75
  dropzoneBackgroundColor="transparent"
71
- icon="iconoirImport"
76
+ icon="iconoirUpload"
72
77
  extraClass="gn-ui-gray-outline-input w-full !h-[32px] px-[8px] py-[4px] justify-between"
73
78
  (fileChange)="handleFileSelected($event)"
74
79
  (errorChange)="handleFileError($event)"
75
80
  data-cy="importGeojson"
76
81
  ></gn-ui-drag-and-drop-file-input>
82
+ <ng-content></ng-content>
77
83
  </div>
78
84
 
79
85
  @if (hasSelection) {
@@ -20,9 +20,9 @@ import { ButtonComponent } from '../button/button.component'
20
20
  import { NgIcon, provideIcons } from '@ng-icons/core'
21
21
  import {
22
22
  iconoirCheckCircle,
23
- iconoirImport,
24
23
  iconoirSquareDashed,
25
24
  iconoirTrash,
25
+ iconoirUpload,
26
26
  } from '@ng-icons/iconoir'
27
27
  import {
28
28
  matClose,
@@ -35,6 +35,7 @@ import {
35
35
  BoundingBox,
36
36
  getGeometryBoundingBox,
37
37
  getGeometryFromGeoJSON,
38
+ isBoundingBoxWithinWorldExtent,
38
39
  propagateToDocumentOnly,
39
40
  readFileAsText,
40
41
  } from '../../../../../../libs/util/shared/src'
@@ -46,11 +47,18 @@ import {
46
47
  marker('search.filters.spatialExtent.import')
47
48
  marker('search.filters.spatialExtent.helpText')
48
49
  marker('search.filters.spatialExtent.error.title')
50
+ marker('search.filters.spatialExtent.error.outOfBounds')
49
51
 
50
52
  const LABEL_FROM_FILE = marker('search.filters.spatialExtent.bboxFromFile')
51
53
  const LABEL_FROM_FILE_DELETE = marker(
52
54
  'search.filters.spatialExtent.bboxFromFileDelete'
53
55
  )
56
+ const LABEL_FROM_LOCATION = marker(
57
+ 'search.filters.spatialExtent.bboxFromLocation'
58
+ )
59
+ const LABEL_FROM_LOCATION_DELETE = marker(
60
+ 'search.filters.spatialExtent.bboxFromLocationDelete'
61
+ )
54
62
  const LABEL_INITIAL = marker('search.filters.spatialExtent.bboxInitial')
55
63
  const LABEL_INITIAL_DELETE = marker(
56
64
  'search.filters.spatialExtent.bboxInitialDelete'
@@ -74,9 +82,9 @@ export interface SpatialExtentDropdownError {
74
82
  providers: [
75
83
  provideIcons({
76
84
  iconoirCheckCircle,
77
- iconoirImport,
78
85
  iconoirSquareDashed,
79
86
  iconoirTrash,
87
+ iconoirUpload,
80
88
  matClose,
81
89
  matExpandLess,
82
90
  matExpandMore,
@@ -103,6 +111,7 @@ export class SpatialExtentDropdownComponent {
103
111
 
104
112
  bbox: BoundingBox | null = null
105
113
  fileName = ''
114
+ locationName = ''
106
115
 
107
116
  @ViewChild('overlayOrigin') overlayOrigin: CdkOverlayOrigin
108
117
  @ViewChild(CdkConnectedOverlay) overlay: CdkConnectedOverlay
@@ -136,15 +145,19 @@ export class SpatialExtentDropdownComponent {
136
145
  }
137
146
 
138
147
  get selectionLabelKey() {
139
- return this.fileName ? LABEL_FROM_FILE : LABEL_INITIAL
148
+ if (this.fileName) return LABEL_FROM_FILE
149
+ if (this.locationName) return LABEL_FROM_LOCATION
150
+ return LABEL_INITIAL
140
151
  }
141
152
 
142
153
  get selectionDeleteLabelKey() {
143
- return this.fileName ? LABEL_FROM_FILE_DELETE : LABEL_INITIAL_DELETE
154
+ if (this.fileName) return LABEL_FROM_FILE_DELETE
155
+ if (this.locationName) return LABEL_FROM_LOCATION_DELETE
156
+ return LABEL_INITIAL_DELETE
144
157
  }
145
158
 
146
159
  get selectionLabelParams() {
147
- return { fileName: this.fileName }
160
+ return { fileName: this.fileName, locationName: this.locationName }
148
161
  }
149
162
 
150
163
  openOverlay() {
@@ -178,8 +191,13 @@ export class SpatialExtentDropdownComponent {
178
191
  return
179
192
  }
180
193
  const bbox = getGeometryBoundingBox(geometry)
194
+ if (!isBoundingBoxWithinWorldExtent(bbox)) {
195
+ this.setError(marker('search.filters.spatialExtent.error.outOfBounds'))
196
+ return
197
+ }
181
198
  this.bbox = bbox
182
199
  this.fileName = file.name
200
+ this.locationName = ''
183
201
  this.bboxChange.emit(bbox)
184
202
  this.cd.markForCheck()
185
203
  } catch {
@@ -188,6 +206,16 @@ export class SpatialExtentDropdownComponent {
188
206
  }
189
207
  }
190
208
 
209
+ setLocationBbox(bbox: BoundingBox, name: string) {
210
+ this.errorKey = null
211
+ this.bbox = bbox
212
+ this.locationName = name
213
+ this.fileName = ''
214
+ this.fileInput?.clear()
215
+ this.bboxChange.emit(bbox)
216
+ this.cd.markForCheck()
217
+ }
218
+
191
219
  handleFileError(error: DragAndDropFileInputError) {
192
220
  if (error === 'file-too-large') {
193
221
  this.setError(marker('search.filters.spatialExtent.error.fileTooLarge'), {
@@ -207,6 +235,7 @@ export class SpatialExtentDropdownComponent {
207
235
  removeSelection(event: Event) {
208
236
  this.bbox = null
209
237
  this.fileName = ''
238
+ this.locationName = ''
210
239
  this.errorKey = null
211
240
  this.fileInput?.clear()
212
241
  this.bboxChange.emit(null)
@@ -3,6 +3,7 @@
3
3
  }
4
4
 
5
5
  :host {
6
+ display: block;
6
7
  position: relative;
7
8
  }
8
9
 
@@ -253,6 +253,9 @@ export function loadAppConfig(configUrl = 'assets/configuration/default.toml') {
253
253
  'advanced_filters',
254
254
  'limit',
255
255
  'spatial_extent_max_file_size',
256
+ 'geocoding_provider',
257
+ 'geocoding_provider_options',
258
+ 'geocoding_provider_labels',
256
259
  ],
257
260
  warnings,
258
261
  errors
@@ -265,6 +268,18 @@ export function loadAppConfig(configUrl = 'assets/configuration/default.toml') {
265
268
  warnings,
266
269
  errors
267
270
  )
271
+ const parsedCustomFilters = parseMultiConfigSection(
272
+ parsed,
273
+ 'custom_filter',
274
+ ['name', 'base_filter'],
275
+ ['exclude_values', 'include_values', 'label_key'],
276
+ warnings,
277
+ errors
278
+ )
279
+ const parsedGeocodingProviderLabels =
280
+ parsedSearchSection?.geocoding_provider_labels as
281
+ | Record<string, string>
282
+ | undefined
268
283
  searchConfig =
269
284
  parsedSearchSection === null
270
285
  ? null
@@ -281,9 +296,28 @@ export function loadAppConfig(configUrl = 'assets/configuration/default.toml') {
281
296
  filters: param.filters,
282
297
  })),
283
298
  ADVANCED_FILTERS: parsedSearchSection.advanced_filters,
299
+ CUSTOM_FILTERS: (parsedCustomFilters ?? []).map(
300
+ (customFilter) => ({
301
+ name: customFilter.name,
302
+ baseFilter: customFilter.base_filter,
303
+ excludeValues: customFilter.exclude_values,
304
+ includeValues: customFilter.include_values,
305
+ labelKey: customFilter.label_key,
306
+ })
307
+ ),
284
308
  LIMIT: parsedSearchSection.limit,
285
309
  SPATIAL_EXTENT_MAX_FILE_SIZE:
286
310
  parsedSearchSection.spatial_extent_max_file_size,
311
+ GEOCODING_PROVIDER: parsedSearchSection.geocoding_provider,
312
+ GEOCODING_PROVIDER_OPTIONS:
313
+ parsedSearchSection.geocoding_provider_options,
314
+ GEOCODING_PROVIDER_LABELS: parsedGeocodingProviderLabels
315
+ ? {
316
+ MAIN: parsedGeocodingProviderLabels.main,
317
+ SECONDARY: parsedGeocodingProviderLabels.secondary,
318
+ TERTIARY: parsedGeocodingProviderLabels.tertiary,
319
+ }
320
+ : undefined,
287
321
  } as SearchConfig)
288
322
 
289
323
  const parsedMetadataQualitySection = parseConfigSection(
@@ -41,7 +41,26 @@ fonts_stylesheet_url = "https://fonts.googleapis.com/css2?family=Open+Sans"
41
41
  record_kind_quick_filter = false
42
42
  filter_geometry_url = 'https://my.domain.org/geom.json'
43
43
  do_not_use_default_search_preset = false
44
- advanced_filters = ['publicationYear', 'documentStandard', 'inspireKeyword', 'topic', 'license']
44
+ geocoding_provider = "geoplateforme"
45
+ geocoding_provider_options.index = "poi"
46
+ geocoding_provider_options.category = "administratif"
47
+ geocoding_provider_options.limit = 5
48
+ geocoding_provider_labels.main = '/properties/name/0'
49
+ geocoding_provider_labels.secondary = '/properties/citycode/0'
50
+ geocoding_provider_labels.tertiary = '/properties/category/1'
51
+ advanced_filters = ['publicationYear', 'documentStandard', 'inspireKeyword', 'topic', 'license', 'myOrg:firstCustomFilter', 'myOrg:secondCustomFilter']
52
+
53
+ [[custom_filter]]
54
+ name = 'myOrg:firstCustomFilter'
55
+ base_filter = 'keyword'
56
+ exclude_values = ['my keyword 1', 'my keyword 2']
57
+ label_key = 'myOrg.firstCustomFilter'
58
+
59
+ [[custom_filter]]
60
+ name = 'myOrg:secondCustomFilter'
61
+ base_filter = 'keyword'
62
+ include_values = ['my keyword 3', 'my keyword 4']
63
+ label_key = 'myOrg.secondCustomFilter'
45
64
 
46
65
  [[search_preset]]
47
66
  sort = "-createDate"
@@ -81,6 +100,23 @@ main_color = "#212029" # All-purpose text color
81
100
  background_color = "#fdfbff"
82
101
  `
83
102
 
103
+ export const customFilterMissingMandatoryConfigFixture = () => `
104
+ [global]
105
+ geonetwork4_api_url = "/geonetwork/srv/api"
106
+
107
+ [theme]
108
+ primary_color = "#093564"
109
+ secondary_color = "#c2e9dc"
110
+ main_color = "#212029" # All-purpose text color
111
+ background_color = "#fdfbff"
112
+
113
+ [search]
114
+ advanced_filters = ['myOrg:firstCustomFilter']
115
+
116
+ [[custom_filter]]
117
+ name = 'myOrg:firstCustomFilter'
118
+ `
119
+
84
120
  export const mapConfigFixture = (): MapConfig => ({
85
121
  MAX_ZOOM: 10,
86
122
  MAX_EXTENT: [-418263.418776, 5251529.591305, 961272.067714, 6706890.609855],
@@ -55,15 +55,38 @@ export interface SearchPreset {
55
55
  filters: Record<string, string[] | string>
56
56
  }
57
57
 
58
+ export type GeocodingProviderName = 'geoadmin' | 'geonames' | 'geoplateforme'
59
+
60
+ export interface CustomSearchFieldConfig {
61
+ name: string
62
+ baseFilter: string
63
+ excludeValues?: string[]
64
+ includeValues?: string[]
65
+ labelKey?: string
66
+ }
67
+
68
+ export interface GeocodingProviderLabelsConfig {
69
+ MAIN?: string
70
+ SECONDARY?: string
71
+ TERTIARY?: string
72
+ }
73
+
58
74
  export interface SearchConfig {
59
75
  RECORD_KIND_QUICK_FILTER?: boolean
60
76
  FILTER_GEOMETRY_URL?: string
61
77
  FILTER_GEOMETRY_DATA?: string
62
78
  DO_NOT_USE_DEFAULT_SEARCH_PRESET?: boolean
63
79
  SEARCH_PRESET?: SearchPreset[]
64
- ADVANCED_FILTERS?: []
80
+ ADVANCED_FILTERS?: string[]
81
+ CUSTOM_FILTERS?: CustomSearchFieldConfig[]
65
82
  LIMIT?: number
66
83
  SPATIAL_EXTENT_MAX_FILE_SIZE?: number
84
+ GEOCODING_PROVIDER?: GeocodingProviderName
85
+ GEOCODING_PROVIDER_OPTIONS?: Record<
86
+ string,
87
+ string | number | boolean | string[]
88
+ >
89
+ GEOCODING_PROVIDER_LABELS?: GeocodingProviderLabelsConfig
67
90
  }
68
91
 
69
92
  export interface MetadataQualityConfig {
@@ -344,7 +344,11 @@ export function getLinkLabel(
344
344
  return format ? `${label} (${format})` : label
345
345
  }
346
346
 
347
- export async function getLayers(url: string, serviceProtocol: ServiceProtocol) {
347
+ export async function getLayers(
348
+ url: string,
349
+ serviceProtocol: ServiceProtocol,
350
+ deep = false
351
+ ) {
348
352
  switch (serviceProtocol) {
349
353
  case 'ogcFeatures': {
350
354
  const layers = await new OgcApiEndpoint(url).allCollections
@@ -352,7 +356,10 @@ export async function getLayers(url: string, serviceProtocol: ServiceProtocol) {
352
356
  }
353
357
  case 'wfs': {
354
358
  const endpointWfs = await new WfsEndpoint(url).isReady()
355
- const featureTypes = await endpointWfs.getFeatureTypes()
359
+ const featureTypes = endpointWfs.getFeatureTypes()
360
+ if (!deep) {
361
+ return featureTypes
362
+ }
356
363
  const layers = (
357
364
  await Promise.allSettled(
358
365
  featureTypes.map((collection) => {
@@ -366,14 +373,16 @@ export async function getLayers(url: string, serviceProtocol: ServiceProtocol) {
366
373
  }
367
374
  case 'wms': {
368
375
  const endpointWms = await new WmsEndpoint(url).isReady()
369
- const layers = (
370
- await endpointWms
371
- .getLayers()
372
- .flatMap(wmsLayerFlatten)
373
- .filter((l) => l.name)
374
- ).map((collection) => {
375
- return endpointWms.getLayerByName(collection.name)
376
- })
376
+ const layersSummary = endpointWms
377
+ .getLayers()
378
+ .flatMap(wmsLayerFlatten)
379
+ .filter((l) => l.name)
380
+ if (!deep) {
381
+ return layersSummary
382
+ }
383
+ const layers = layersSummary.map((collection) =>
384
+ endpointWms.getLayerByName(collection.name)
385
+ )
377
386
  return layers
378
387
  }
379
388
  case 'wmts': {
@@ -46,6 +46,22 @@ export function isBoundingBox(value: unknown): value is BoundingBox {
46
46
  )
47
47
  }
48
48
 
49
+ /**
50
+ * Checks that a bounding box's coordinates stay within the valid
51
+ * longitude (-180/180) and latitude (-90/90) ranges.
52
+ */
53
+ export function isBoundingBoxWithinWorldExtent(bbox: BoundingBox): boolean {
54
+ const [minX, minY, maxX, maxY] = bbox
55
+ return (
56
+ minX >= -180 &&
57
+ maxX <= 180 &&
58
+ minY >= -90 &&
59
+ maxY <= 90 &&
60
+ minX <= maxX &&
61
+ minY <= maxY
62
+ )
63
+ }
64
+
49
65
  export function getGeometryBoundingBox(geometry: Geometry): BoundingBox {
50
66
  // use the bounding box if specified in the GeoJSON object
51
67
  if (geometry.bbox) {
@@ -372,7 +372,7 @@
372
372
  "favorite.starToggle.add": "Zu Favoriten hinzufügen",
373
373
  "favorite.starToggle.remove": "Aus Favoriten entfernen",
374
374
  "feature.catalog.attribute.code": "Code",
375
- "feature.catalog.attribute.definition": "Beschreibung",
375
+ "feature.catalog.attribute.description": "Beschreibung",
376
376
  "feature.catalog.attribute.name": "Name",
377
377
  "feature.catalog.attribute.type": "Typ",
378
378
  "feature.catalog.attribute.values": "Werte",
@@ -425,7 +425,6 @@
425
425
  "map.add.layer.wms": "Aus WMS",
426
426
  "map.addFromFile.placeholder": "Klicke hier oder ziehe eine Datei herein",
427
427
  "map.dropdown.placeholder": "Kein Vorschaulayer verfügbar",
428
- "map.geocoding.placeholder": "Nach einem Ort suchen",
429
428
  "map.help.addFromFile": "Klicke oder ziehe eine Datei herein um eine Karte hinzuzufügen (momentan wird nur das GeoJSON-Format unterstützt).",
430
429
  "map.layer.add": "Hinzufügen",
431
430
  "map.layers.available": "Verfügbare Layer",
@@ -659,6 +658,7 @@
659
658
  "search.filters.changeDate": "Letzte Aktualisierung",
660
659
  "search.filters.clear": "Zurücksetzen",
661
660
  "search.filters.contact": "Kontakte",
661
+ "search.filters.documentStandard": "Dokumentenstandard",
662
662
  "search.filters.format": "Formate",
663
663
  "search.filters.inspireKeyword": "INSPIRE-Schlüsselwort",
664
664
  "search.filters.isSpatial": "Ist räumliche Daten",
@@ -690,21 +690,29 @@
690
690
  "search.filters.recordKind.reuse": "Wiederverwendungen",
691
691
  "search.filters.recordKind.service": "Dienste",
692
692
  "search.filters.representationType": "Repräsentationstyp",
693
+ "search.filters.resourceCreationRevisionDate": "Ressourcendatum",
693
694
  "search.filters.resourceType": "Ressourcentyp",
694
695
  "search.filters.spatialExtent": "",
695
696
  "search.filters.spatialExtent.bboxFromFile": "",
696
697
  "search.filters.spatialExtent.bboxFromFileDelete": "",
698
+ "search.filters.spatialExtent.bboxFromLocation": "",
699
+ "search.filters.spatialExtent.bboxFromLocationDelete": "",
697
700
  "search.filters.spatialExtent.bboxInitial": "",
698
701
  "search.filters.spatialExtent.bboxInitialDelete": "",
699
702
  "search.filters.spatialExtent.error.fileTooLarge": "",
700
703
  "search.filters.spatialExtent.error.invalidFormat": "",
701
704
  "search.filters.spatialExtent.error.noGeometry": "",
705
+ "search.filters.spatialExtent.error.outOfBounds": "",
702
706
  "search.filters.spatialExtent.error.title": "",
703
707
  "search.filters.spatialExtent.helpText": "",
704
708
  "search.filters.spatialExtent.import": "",
709
+ "search.filters.spatialExtent.searchLocation": "",
705
710
  "search.filters.standard": "Standard",
706
711
  "search.filters.summaryLabel.changeDate": "Geändert am: ",
712
+ "search.filters.summaryLabel.resourceCreationRevisionDate": "Ressourcendatum: ",
713
+ "search.filters.summaryLabel.temporalExtent": "Zeitlicher Umfang: ",
707
714
  "search.filters.summaryLabel.user": "Geändert von: ",
715
+ "search.filters.temporalExtent": "Zeitlicher Umfang",
708
716
  "search.filters.title": "Ergebnisse filtern",
709
717
  "search.filters.topic": "Themen",
710
718
  "search.filters.useSpatialFilter": "Zuerst Ressourcen im Interessenbereich anzeigen",
@@ -372,7 +372,7 @@
372
372
  "favorite.starToggle.add": "Add to favorites",
373
373
  "favorite.starToggle.remove": "Remove from favorites",
374
374
  "feature.catalog.attribute.code": "Code",
375
- "feature.catalog.attribute.definition": "Description",
375
+ "feature.catalog.attribute.description": "Description",
376
376
  "feature.catalog.attribute.name": "Name",
377
377
  "feature.catalog.attribute.type": "Type",
378
378
  "feature.catalog.attribute.values": "Values",
@@ -425,7 +425,6 @@
425
425
  "map.add.layer.wms": "From WMS",
426
426
  "map.addFromFile.placeholder": "Click or drop a file here",
427
427
  "map.dropdown.placeholder": "No preview layer available",
428
- "map.geocoding.placeholder": "Search for a place",
429
428
  "map.help.addFromFile": "Click or drag and drop a file to add to the map (currently supports GeoJSON format only).",
430
429
  "map.layer.add": "Add",
431
430
  "map.layers.available": "Available Layers",
@@ -659,6 +658,7 @@
659
658
  "search.filters.changeDate": "Updated",
660
659
  "search.filters.clear": "Reset",
661
660
  "search.filters.contact": "Contacts",
661
+ "search.filters.documentStandard": "Document standard",
662
662
  "search.filters.format": "Formats",
663
663
  "search.filters.inspireKeyword": "INSPIRE keyword",
664
664
  "search.filters.isSpatial": "Is spatial data",
@@ -690,21 +690,29 @@
690
690
  "search.filters.recordKind.reuse": "Reuses",
691
691
  "search.filters.recordKind.service": "Services",
692
692
  "search.filters.representationType": "Representation type",
693
+ "search.filters.resourceCreationRevisionDate": "Resource date",
693
694
  "search.filters.resourceType": "Resource type",
694
695
  "search.filters.spatialExtent": "Spatial extent",
695
- "search.filters.spatialExtent.bboxFromFile": "extent from {fileName}",
696
+ "search.filters.spatialExtent.bboxFromFile": "Extent from {fileName}",
696
697
  "search.filters.spatialExtent.bboxFromFileDelete": "Delete {fileName}",
698
+ "search.filters.spatialExtent.bboxFromLocation": "Extent from {locationName}",
699
+ "search.filters.spatialExtent.bboxFromLocationDelete": "Delete {locationName}",
697
700
  "search.filters.spatialExtent.bboxInitial": "Pre-selected extent",
698
701
  "search.filters.spatialExtent.bboxInitialDelete": "Delete selection",
699
702
  "search.filters.spatialExtent.error.fileTooLarge": "The file exceeds the maximum allowed size ({maxSize} MB)",
700
703
  "search.filters.spatialExtent.error.invalidFormat": "The file could not be read as a valid GeoJSON",
701
704
  "search.filters.spatialExtent.error.noGeometry": "No geometry could be found in the file",
705
+ "search.filters.spatialExtent.error.outOfBounds": "The geometry of the GeoJSON file is not compliant; either some coordinates are out of bounds ([-180, 180] for longitude and [-90, 90] for latitude), or the file uses projected coordinates instead of longitude and latitude",
702
706
  "search.filters.spatialExtent.error.title": "GeoJSON import error",
703
- "search.filters.spatialExtent.helpText": "The selected item for the spatial extent will be converted to a bbox",
707
+ "search.filters.spatialExtent.helpText": "The extent of the selected geometry will be used",
704
708
  "search.filters.spatialExtent.import": "Import a GeoJSON",
709
+ "search.filters.spatialExtent.searchLocation": "Search in the list",
705
710
  "search.filters.standard": "Standard",
706
711
  "search.filters.summaryLabel.changeDate": "Updated: ",
712
+ "search.filters.summaryLabel.resourceCreationRevisionDate": "Resource date: ",
713
+ "search.filters.summaryLabel.temporalExtent": "Temporal extent: ",
707
714
  "search.filters.summaryLabel.user": "Editor: ",
715
+ "search.filters.temporalExtent": "Temporal extent",
708
716
  "search.filters.title": "Filter your results",
709
717
  "search.filters.topic": "Topics",
710
718
  "search.filters.useSpatialFilter": "Show records in the area of interest first",