geonetwork-ui 2.11.0-dev.1cf4ab0c6 → 2.11.0-dev.448254758

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 (43) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1138 -1249
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +79 -67
  4. package/index.d.ts.map +1 -1
  5. package/package.json +1 -1
  6. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +40 -9
  7. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +8 -1
  8. package/src/libs/feature/map/src/index.ts +0 -2
  9. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  10. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
  11. package/src/libs/feature/search/src/index.ts +2 -0
  12. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +8 -3
  13. package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +16 -7
  14. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +8 -0
  15. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +40 -0
  16. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  17. package/src/libs/feature/search/src/lib/utils/service/fields.ts +25 -7
  18. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +10 -1
  19. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +8 -0
  20. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  21. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  22. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  23. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +2 -2
  24. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +8 -2
  25. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  26. package/src/libs/util/app-config/src/lib/app-config.ts +20 -0
  27. package/src/libs/util/app-config/src/lib/fixtures.ts +16 -1
  28. package/src/libs/util/app-config/src/lib/model.ts +14 -1
  29. package/src/libs/util/shared/src/lib/utils/geojson.ts +16 -0
  30. package/translations/de.json +2 -2
  31. package/translations/en.json +4 -4
  32. package/translations/es.json +2 -2
  33. package/translations/fr.json +5 -5
  34. package/translations/it.json +2 -2
  35. package/translations/nl.json +2 -2
  36. package/translations/pt.json +2 -2
  37. package/translations/sk.json +2 -2
  38. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.css +0 -0
  39. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  40. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  41. package/src/libs/feature/map/src/lib/style/index.ts +0 -2
  42. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  43. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ChangeDetectionStrategy,
3
3
  Component,
4
+ InjectionToken,
4
5
  Input,
5
6
  OnInit,
6
7
  inject,
@@ -27,7 +28,12 @@ import { CommonModule } from '@angular/common'
27
28
  import { BoundingBox } from '../../../../../../libs/util/shared/src'
28
29
  import { NotificationsService } from '../../../../../../libs/feature/notifications/src'
29
30
  import { TranslateService } from '@ngx-translate/core'
30
- import { getOptionalSearchConfig } from '../../../../../../libs/util/app-config/src'
31
+
32
+ // in MB, null means no limit
33
+ export const SPATIAL_EXTENT_MAX_FILE_SIZE = new InjectionToken<number | null>(
34
+ 'spatialExtentMaxFileSize',
35
+ { factory: () => null }
36
+ )
31
37
 
32
38
  @Component({
33
39
  selector: 'gn-ui-filter-dropdown',
@@ -52,8 +58,7 @@ export class FilterDropdownComponent implements OnInit {
52
58
  @Input() fieldName: string
53
59
  @Input() title: string
54
60
 
55
- spatialExtentMaxFileSize =
56
- getOptionalSearchConfig()?.SPATIAL_EXTENT_MAX_FILE_SIZE
61
+ spatialExtentMaxFileSize = inject(SPATIAL_EXTENT_MAX_FILE_SIZE)
57
62
 
58
63
  fieldType: FieldType
59
64
  choices$: Observable<Choice[]>
@@ -5,22 +5,31 @@ import {
5
5
  GeocodingResult,
6
6
  queryGeonames,
7
7
  GeonamesOptions,
8
- DataGouvFrOptions,
9
- queryDataGouvFr,
8
+ GeoplateformeOptions,
9
+ queryGeoplateforme,
10
10
  } from '@geospatial-sdk/geocoding'
11
11
  import { from, Observable, throwError } from 'rxjs'
12
12
  import { catchError } from 'rxjs/operators'
13
13
 
14
14
  type GeoadminGeocodingProvider = ['geoadmin', GeoadminOptions]
15
15
  type GeonamesGeocodingProvider = ['geonames', GeonamesOptions]
16
- type DataGouvFrGeocodingProvider = ['data-gouv-fr', DataGouvFrOptions]
16
+ type GeoplateformeGeocodingProvider = ['geoplateforme', GeoplateformeOptions]
17
17
  export type GeocodingProvider =
18
18
  | GeoadminGeocodingProvider
19
19
  | GeonamesGeocodingProvider
20
- | DataGouvFrGeocodingProvider
20
+ | GeoplateformeGeocodingProvider
21
+
22
+ export const DEFAULT_GEOCODING_PROVIDER = [
23
+ 'geonames',
24
+ { maxRows: 5 },
25
+ ] as GeocodingProvider
21
26
 
22
27
  export const GEOCODING_PROVIDER = new InjectionToken<GeocodingProvider>(
23
- 'geocoding-provider'
28
+ 'geocoding-provider',
29
+ {
30
+ providedIn: 'root',
31
+ factory: (): GeocodingProvider => DEFAULT_GEOCODING_PROVIDER,
32
+ }
24
33
  )
25
34
 
26
35
  @Injectable({
@@ -42,9 +51,9 @@ export class GeocodingService {
42
51
  queryGeonames(text, this.provider[1] as GeonamesOptions)
43
52
  )
44
53
  break
45
- case 'data-gouv-fr':
54
+ case 'geoplateforme':
46
55
  queryObservable = from(
47
- queryDataGouvFr(text, this.provider[1] as DataGouvFrOptions)
56
+ queryGeoplateforme(text, this.provider[1] as GeoplateformeOptions)
48
57
  )
49
58
  break
50
59
  default:
@@ -0,0 +1,8 @@
1
+ <gn-ui-autocomplete
2
+ [action]="searchAction"
3
+ [displayWithFn]="displayWithFn"
4
+ [placeholder]="placeholder"
5
+ [displayWithTemplate]="displayWithTemplate"
6
+ (itemSelected)="handleItemSelected($event)"
7
+ >
8
+ </gn-ui-autocomplete>
@@ -0,0 +1,40 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ EventEmitter,
5
+ inject,
6
+ Input,
7
+ Output,
8
+ TemplateRef,
9
+ } from '@angular/core'
10
+ import { Observable } from 'rxjs'
11
+ import { GeocodingResult } from '@geospatial-sdk/geocoding'
12
+ import {
13
+ AutocompleteComponent,
14
+ AutocompleteItem,
15
+ } from '../../../../../../libs/ui/inputs/src'
16
+ import { GeocodingService } from '../geocoding/geocoding.service'
17
+
18
+ @Component({
19
+ selector: 'gn-ui-location-search',
20
+ templateUrl: './location-search.component.html',
21
+ changeDetection: ChangeDetectionStrategy.OnPush,
22
+ standalone: true,
23
+ imports: [AutocompleteComponent],
24
+ })
25
+ export class LocationSearchComponent {
26
+ private geocodingService = inject(GeocodingService)
27
+
28
+ @Input() placeholder = ''
29
+ @Input() displayWithTemplate?: TemplateRef<{ $implicit: GeocodingResult }>
30
+ @Output() resultSelected = new EventEmitter<GeocodingResult>()
31
+
32
+ displayWithFn = (item: AutocompleteItem) => (item as GeocodingResult).label
33
+
34
+ searchAction = (text: string): Observable<AutocompleteItem[]> =>
35
+ this.geocodingService.query(text)
36
+
37
+ handleItemSelected(item: AutocompleteItem) {
38
+ this.resultSelected.emit(item as GeocodingResult)
39
+ }
40
+ }
@@ -8,7 +8,6 @@ import {
8
8
  SortByField,
9
9
  } from '../../../../../../libs/common/domain/src/lib/model/search'
10
10
  import { DEFAULT_PAGE_SIZE, FIELDS_SUMMARY } from '../constants'
11
- import { getOptionalSearchConfig } from '../../../../../../libs/util/app-config/src'
12
11
  import { CatalogRecord } from '../../../../../../libs/common/domain/src/lib/model/record'
13
12
 
14
13
  export const SEARCH_FEATURE_KEY = 'searchState'
@@ -56,9 +55,7 @@ export const initSearch = (): SearchStateSearch => {
56
55
  },
57
56
  params: {
58
57
  filters: {},
59
- pageSize: getOptionalSearchConfig()?.LIMIT
60
- ? getOptionalSearchConfig().LIMIT
61
- : DEFAULT_PAGE_SIZE,
58
+ pageSize: DEFAULT_PAGE_SIZE,
62
59
  currentPage: 0,
63
60
  favoritesOnly: false,
64
61
  useSpatialFilter: true,
@@ -435,17 +435,35 @@ export class DateRangeSearchField extends SimpleSearchField {
435
435
  }
436
436
  }
437
437
 
438
+ // treats a resource's creation/revision dates as an interval (its min and max), so a record matches
439
+ // when that interval intersects the filter range, not just when a single date falls inside it
438
440
  export class ResourceCreationRevisionDateSearchField extends DateRangeSearchField {
439
441
  constructor(injector: Injector, order: 'asc' | 'desc' = 'desc') {
440
442
  super('resourceCreationRevisionDate', injector, order)
443
+ const dateSourceFields = [
444
+ 'creationDateForResource',
445
+ 'revisionDateForResource',
446
+ ]
447
+ const collectSortedDates = dateSourceFields
448
+ .map(
449
+ (field) => `if (doc.containsKey('${field}')) {
450
+ for (def date : doc['${field}']) { dates.add(date.millis); }
451
+ }`
452
+ )
453
+ .join('\n')
454
+
441
455
  this.esService.registerRuntimeField(
442
- 'resourceCreationRevisionDate',
443
- `if (doc.containsKey('creationDateForResource') && doc['creationDateForResource'].size() > 0) {
444
- for (def date : doc['creationDateForResource']) { emit(date.millis); }
445
- }
446
- if (doc.containsKey('revisionDateForResource') && doc['revisionDateForResource'].size() > 0) {
447
- for (def date : doc['revisionDateForResource']) { emit(date.millis); }
448
- }`,
456
+ 'resourceCreationRevisionDateMin',
457
+ `def dates = [];
458
+ ${collectSortedDates}
459
+ if (dates.length > 0) { dates.sort(null); emit(dates[0]); }`,
460
+ 'date'
461
+ )
462
+ this.esService.registerRuntimeField(
463
+ 'resourceCreationRevisionDateMax',
464
+ `def dates = [];
465
+ ${collectSortedDates}
466
+ if (dates.length > 0) { dates.sort(null); emit(dates[dates.length - 1]); }`,
449
467
  'date'
450
468
  )
451
469
  }
@@ -83,7 +83,16 @@
83
83
  >
84
84
  @for (suggestion of suggestions$ | async; track suggestion) {
85
85
  <mat-option [value]="suggestion" class="p-2 suggestion text-start">
86
- {{ displayWithFnInternal(suggestion) }}
86
+ @if (displayWithTemplate) {
87
+ <ng-container
88
+ *ngTemplateOutlet="
89
+ displayWithTemplate;
90
+ context: { $implicit: suggestion }
91
+ "
92
+ ></ng-container>
93
+ } @else {
94
+ {{ displayWithFnInternal(suggestion) }}
95
+ }
87
96
  </mat-option>
88
97
  }
89
98
  </mat-autocomplete>
@@ -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()
@@ -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
  }
@@ -58,7 +58,7 @@
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'
62
62
  "
63
63
  >
64
64
  <gn-ui-drag-and-drop-file-input
@@ -68,7 +68,7 @@
68
68
  [showFileName]="false"
69
69
  textClass="text-black truncate"
70
70
  dropzoneBackgroundColor="transparent"
71
- icon="iconoirImport"
71
+ icon="iconoirUpload"
72
72
  extraClass="gn-ui-gray-outline-input w-full !h-[32px] px-[8px] py-[4px] justify-between"
73
73
  (fileChange)="handleFileSelected($event)"
74
74
  (errorChange)="handleFileError($event)"
@@ -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,6 +47,7 @@ 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(
@@ -74,9 +76,9 @@ export interface SpatialExtentDropdownError {
74
76
  providers: [
75
77
  provideIcons({
76
78
  iconoirCheckCircle,
77
- iconoirImport,
78
79
  iconoirSquareDashed,
79
80
  iconoirTrash,
81
+ iconoirUpload,
80
82
  matClose,
81
83
  matExpandLess,
82
84
  matExpandMore,
@@ -178,6 +180,10 @@ export class SpatialExtentDropdownComponent {
178
180
  return
179
181
  }
180
182
  const bbox = getGeometryBoundingBox(geometry)
183
+ if (!isBoundingBoxWithinWorldExtent(bbox)) {
184
+ this.setError(marker('search.filters.spatialExtent.error.outOfBounds'))
185
+ return
186
+ }
181
187
  this.bbox = bbox
182
188
  this.fileName = file.name
183
189
  this.bboxChange.emit(bbox)
@@ -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,8 @@ 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',
256
258
  ],
257
259
  warnings,
258
260
  errors
@@ -265,6 +267,14 @@ export function loadAppConfig(configUrl = 'assets/configuration/default.toml') {
265
267
  warnings,
266
268
  errors
267
269
  )
270
+ const parsedCustomFilters = parseMultiConfigSection(
271
+ parsed,
272
+ 'custom_filter',
273
+ ['name', 'base_filter'],
274
+ ['exclude_values', 'include_values', 'label_key'],
275
+ warnings,
276
+ errors
277
+ )
268
278
  searchConfig =
269
279
  parsedSearchSection === null
270
280
  ? null
@@ -281,9 +291,19 @@ export function loadAppConfig(configUrl = 'assets/configuration/default.toml') {
281
291
  filters: param.filters,
282
292
  })),
283
293
  ADVANCED_FILTERS: parsedSearchSection.advanced_filters,
294
+ CUSTOM_FILTERS: parsedCustomFilters.map((customFilter) => ({
295
+ name: customFilter.name,
296
+ baseFilter: customFilter.base_filter,
297
+ excludeValues: customFilter.exclude_values,
298
+ includeValues: customFilter.include_values,
299
+ labelKey: customFilter.label_key,
300
+ })),
284
301
  LIMIT: parsedSearchSection.limit,
285
302
  SPATIAL_EXTENT_MAX_FILE_SIZE:
286
303
  parsedSearchSection.spatial_extent_max_file_size,
304
+ GEOCODING_PROVIDER: parsedSearchSection.geocoding_provider,
305
+ GEOCODING_PROVIDER_OPTIONS:
306
+ parsedSearchSection.geocoding_provider_options,
287
307
  } as SearchConfig)
288
308
 
289
309
  const parsedMetadataQualitySection = parseConfigSection(
@@ -41,7 +41,22 @@ 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.category = "administratif"
46
+ geocoding_provider_options.limit = 5
47
+ advanced_filters = ['publicationYear', 'documentStandard', 'inspireKeyword', 'topic', 'license', 'myOrg:firstCustomFilter', 'myOrg:secondCustomFilter']
48
+
49
+ [[custom_filter]]
50
+ name = 'myOrg:firstCustomFilter'
51
+ base_filter = 'keyword'
52
+ exclude_values = ['my keyword 1', 'my keyword 2']
53
+ label_key = 'myOrg.firstCustomFilter'
54
+
55
+ [[custom_filter]]
56
+ name = 'myOrg:secondCustomFilter'
57
+ base_filter = 'keyword'
58
+ include_values = ['my keyword 3', 'my keyword 4']
59
+ label_key = 'myOrg.secondCustomFilter'
45
60
 
46
61
  [[search_preset]]
47
62
  sort = "-createDate"
@@ -55,15 +55,28 @@ 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 CustomSearchFilterField {
61
+ name: string
62
+ baseFilter: string
63
+ excludeValues?: string[]
64
+ includeValues?: string[]
65
+ labelKey?: string
66
+ }
67
+
58
68
  export interface SearchConfig {
59
69
  RECORD_KIND_QUICK_FILTER?: boolean
60
70
  FILTER_GEOMETRY_URL?: string
61
71
  FILTER_GEOMETRY_DATA?: string
62
72
  DO_NOT_USE_DEFAULT_SEARCH_PRESET?: boolean
63
73
  SEARCH_PRESET?: SearchPreset[]
64
- ADVANCED_FILTERS?: []
74
+ ADVANCED_FILTERS?: string[]
75
+ CUSTOM_FILTERS?: CustomSearchFilterField[]
65
76
  LIMIT?: number
66
77
  SPATIAL_EXTENT_MAX_FILE_SIZE?: number
78
+ GEOCODING_PROVIDER?: GeocodingProviderName
79
+ GEOCODING_PROVIDER_OPTIONS?: Record<string, string | number>
67
80
  }
68
81
 
69
82
  export interface MetadataQualityConfig {
@@ -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) {
@@ -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",
@@ -692,7 +691,6 @@
692
691
  "search.filters.representationType": "Repräsentationstyp",
693
692
  "search.filters.resourceCreationRevisionDate": "Ressourcendatum",
694
693
  "search.filters.resourceType": "Ressourcentyp",
695
- "search.filters.spatialExtent": "",
696
694
  "search.filters.spatialExtent.bboxFromFile": "",
697
695
  "search.filters.spatialExtent.bboxFromFileDelete": "",
698
696
  "search.filters.spatialExtent.bboxInitial": "",
@@ -700,7 +698,9 @@
700
698
  "search.filters.spatialExtent.error.fileTooLarge": "",
701
699
  "search.filters.spatialExtent.error.invalidFormat": "",
702
700
  "search.filters.spatialExtent.error.noGeometry": "",
701
+ "search.filters.spatialExtent.error.outOfBounds": "",
703
702
  "search.filters.spatialExtent.error.title": "",
703
+ "search.filters.spatialExtent": "",
704
704
  "search.filters.spatialExtent.helpText": "",
705
705
  "search.filters.spatialExtent.import": "",
706
706
  "search.filters.standard": "Standard",
@@ -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",
@@ -692,16 +691,17 @@
692
691
  "search.filters.representationType": "Representation type",
693
692
  "search.filters.resourceCreationRevisionDate": "Resource date",
694
693
  "search.filters.resourceType": "Resource type",
695
- "search.filters.spatialExtent": "Spatial extent",
696
- "search.filters.spatialExtent.bboxFromFile": "extent from {fileName}",
694
+ "search.filters.spatialExtent.bboxFromFile": "Extent from {fileName}",
697
695
  "search.filters.spatialExtent.bboxFromFileDelete": "Delete {fileName}",
698
696
  "search.filters.spatialExtent.bboxInitial": "Pre-selected extent",
699
697
  "search.filters.spatialExtent.bboxInitialDelete": "Delete selection",
700
698
  "search.filters.spatialExtent.error.fileTooLarge": "The file exceeds the maximum allowed size ({maxSize} MB)",
701
699
  "search.filters.spatialExtent.error.invalidFormat": "The file could not be read as a valid GeoJSON",
702
700
  "search.filters.spatialExtent.error.noGeometry": "No geometry could be found in the file",
701
+ "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",
703
702
  "search.filters.spatialExtent.error.title": "GeoJSON import error",
704
- "search.filters.spatialExtent.helpText": "The selected item for the spatial extent will be converted to a bbox",
703
+ "search.filters.spatialExtent": "Spatial extent",
704
+ "search.filters.spatialExtent.helpText": "The extent of the selected geometry will be used",
705
705
  "search.filters.spatialExtent.import": "Import a GeoJSON",
706
706
  "search.filters.standard": "Standard",
707
707
  "search.filters.summaryLabel.changeDate": "Updated: ",
@@ -425,7 +425,6 @@
425
425
  "map.add.layer.wms": "",
426
426
  "map.addFromFile.placeholder": "",
427
427
  "map.dropdown.placeholder": "",
428
- "map.geocoding.placeholder": "",
429
428
  "map.help.addFromFile": "",
430
429
  "map.layer.add": "",
431
430
  "map.layers.available": "",
@@ -692,7 +691,6 @@
692
691
  "search.filters.representationType": "",
693
692
  "search.filters.resourceCreationRevisionDate": "",
694
693
  "search.filters.resourceType": "",
695
- "search.filters.spatialExtent": "",
696
694
  "search.filters.spatialExtent.bboxFromFile": "",
697
695
  "search.filters.spatialExtent.bboxFromFileDelete": "",
698
696
  "search.filters.spatialExtent.bboxInitial": "",
@@ -700,7 +698,9 @@
700
698
  "search.filters.spatialExtent.error.fileTooLarge": "",
701
699
  "search.filters.spatialExtent.error.invalidFormat": "",
702
700
  "search.filters.spatialExtent.error.noGeometry": "",
701
+ "search.filters.spatialExtent.error.outOfBounds": "",
703
702
  "search.filters.spatialExtent.error.title": "",
703
+ "search.filters.spatialExtent": "",
704
704
  "search.filters.spatialExtent.helpText": "",
705
705
  "search.filters.spatialExtent.import": "",
706
706
  "search.filters.standard": "",
@@ -425,7 +425,6 @@
425
425
  "map.add.layer.wms": "Depuis un service WMS",
426
426
  "map.addFromFile.placeholder": "Cliquez ou déposez un fichier ici",
427
427
  "map.dropdown.placeholder": "Aucune couche d'aperçu disponible",
428
- "map.geocoding.placeholder": "Rechercher un lieu",
429
428
  "map.help.addFromFile": "Cliquez ou glissez-déposez un fichier pour l'ajouter à la carte (supporte actuellement uniquement le format GeoJSON).",
430
429
  "map.layer.add": "Ajouter",
431
430
  "map.layers.available": "Couches disponibles",
@@ -692,16 +691,17 @@
692
691
  "search.filters.representationType": "Type de représentation",
693
692
  "search.filters.resourceCreationRevisionDate": "Date de la ressource",
694
693
  "search.filters.resourceType": "Type de ressource",
695
- "search.filters.spatialExtent": "Étendue spatiale",
696
- "search.filters.spatialExtent.bboxFromFile": "étendue de {fileName}",
694
+ "search.filters.spatialExtent.bboxFromFile": "Étendue de {fileName}",
697
695
  "search.filters.spatialExtent.bboxFromFileDelete": "Supprimer {fileName}",
698
- "search.filters.spatialExtent.bboxInitial": "étendue présélectionnée",
696
+ "search.filters.spatialExtent.bboxInitial": "Étendue présélectionnée",
699
697
  "search.filters.spatialExtent.bboxInitialDelete": "Supprimer la sélection",
700
698
  "search.filters.spatialExtent.error.fileTooLarge": "Le fichier dépasse la taille maximale autorisée ({maxSize} Mo)",
701
699
  "search.filters.spatialExtent.error.invalidFormat": "Le fichier n'a pas pu être lu comme un GeoJSON valide",
702
700
  "search.filters.spatialExtent.error.noGeometry": "Aucune géométrie n'a pu être trouvée dans le fichier",
701
+ "search.filters.spatialExtent.error.outOfBounds": "La géométrie du fichier GeoJSON n'est pas conforme ; soit certaines coordonnées sont hors limites ([-180, 180] pour la longitude et [-90, 90] pour la latitude), soit le fichier utilise des coordonnées projetées au lieu de la longitude et de la latitude.",
703
702
  "search.filters.spatialExtent.error.title": "Erreur d'importation GeoJSON",
704
- "search.filters.spatialExtent.helpText": "L’élément sélectionné pour l'étendue sera converti en bbox",
703
+ "search.filters.spatialExtent": "Étendue spatiale",
704
+ "search.filters.spatialExtent.helpText": "L'étendue de la géométrie sélectionnée sera utilisée",
705
705
  "search.filters.spatialExtent.import": "Importer un GeoJSON",
706
706
  "search.filters.standard": "Standard",
707
707
  "search.filters.summaryLabel.changeDate": "Mise à jour : ",