geonetwork-ui 2.11.0-dev.e7b8ec331 → 2.11.0-dev.f5ee66b24

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 (91) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1482 -928
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +280 -124
  4. package/index.d.ts.map +1 -1
  5. package/package.json +12 -11
  6. package/src/libs/api/metadata-converter/src/lib/gn4/gn4.field.mapper.ts +43 -13
  7. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +108 -34
  8. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +40 -16
  9. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +12 -0
  10. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +20 -1
  11. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -2
  12. package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
  13. package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
  14. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +22 -22
  15. package/src/libs/feature/map/src/index.ts +0 -1
  16. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  17. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  18. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  19. package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
  20. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
  21. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
  22. package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
  23. package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
  24. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  25. package/src/libs/feature/search/src/index.ts +2 -0
  26. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +12 -4
  27. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +58 -19
  28. package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +5 -1
  29. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +8 -0
  30. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +40 -0
  31. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +18 -8
  32. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +9 -1
  33. package/src/libs/feature/search/src/lib/utils/service/fields.ts +61 -2
  34. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  35. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  36. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  37. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  38. package/src/libs/ui/inputs/src/index.ts +2 -0
  39. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +10 -1
  40. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +8 -0
  41. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  42. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  43. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  44. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  45. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  46. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  47. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  48. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  49. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  50. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  51. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  52. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  53. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
  54. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -0
  55. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  56. package/src/libs/util/app-config/src/lib/app-config.ts +3 -0
  57. package/src/libs/util/app-config/src/lib/model.ts +1 -0
  58. package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
  59. package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
  60. package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
  61. package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
  62. package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
  63. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
  64. package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
  65. package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
  66. package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
  67. package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
  68. package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
  69. package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
  70. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
  71. package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
  72. package/src/libs/util/shared/src/index.ts +1 -0
  73. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  74. package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
  75. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  76. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  77. package/src/libs/util/shared/src/lib/utils/geojson.ts +8 -0
  78. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  79. package/tailwind.base.css +5 -0
  80. package/translations/de.json +17 -3
  81. package/translations/en.json +16 -2
  82. package/translations/es.json +15 -1
  83. package/translations/fr.json +17 -3
  84. package/translations/it.json +15 -1
  85. package/translations/nl.json +15 -1
  86. package/translations/pt.json +15 -1
  87. package/translations/sk.json +15 -1
  88. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  89. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  90. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
  91. /package/src/libs/{feature/map/src/lib/geocoding/geocoding.component.css → ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css} +0 -0
@@ -4,6 +4,7 @@ import { DatavizChartConfigModel } from '../../../../../../libs/common/domain/sr
4
4
  import {
5
5
  CatalogRecord,
6
6
  DatasetFeatureCatalog,
7
+ LinkedRecord,
7
8
  UserFeedback,
8
9
  } from '../../../../../../libs/common/domain/src/lib/model/record'
9
10
 
@@ -14,8 +15,7 @@ export interface MetadataViewState {
14
15
  error: { notFound?: boolean; otherError?: string } | null
15
16
  metadata?: Partial<CatalogRecord>
16
17
  related?: CatalogRecord[]
17
- sources?: CatalogRecord[]
18
- sourceOf?: CatalogRecord[]
18
+ linkedRecords?: LinkedRecord[]
19
19
  userFeedbacks?: UserFeedback[]
20
20
  allUserFeedbacksLoading: boolean
21
21
  addUserFeedbackLoading: boolean
@@ -77,14 +77,9 @@ const metadataViewReducer = createReducer(
77
77
  related,
78
78
  })),
79
79
 
80
- on(MetadataViewActions.setSources, (state, { sources }) => ({
80
+ on(MetadataViewActions.setLinkedRecords, (state, { linkedRecords }) => ({
81
81
  ...state,
82
- sources,
83
- })),
84
-
85
- on(MetadataViewActions.setSourceOf, (state, { sourceOf }) => ({
86
- ...state,
87
- sourceOf,
82
+ linkedRecords,
88
83
  })),
89
84
 
90
85
  /*
@@ -41,14 +41,9 @@ export const getRelated = createSelector(
41
41
  (state: MetadataViewState) => state.related
42
42
  )
43
43
 
44
- export const getSources = createSelector(
44
+ export const getLinkedRecords = createSelector(
45
45
  getMdViewState,
46
- (state: MetadataViewState) => state.sources
47
- )
48
-
49
- export const getSourceOf = createSelector(
50
- getMdViewState,
51
- (state: MetadataViewState) => state.sourceOf
46
+ (state: MetadataViewState) => state.linkedRecords
52
47
  )
53
48
  /*
54
49
  Metadata selectors
@@ -22,7 +22,7 @@ export function flattenQueryParams(
22
22
  const start = (flattened[key] as DateRange).start
23
23
  const end = (flattened[key] as DateRange).end
24
24
  flattened[key] = [
25
- `${start ? formatDate(start) : ''}..${formatDate(end) || ''}`,
25
+ `${start ? formatDate(start) : ''}..${end ? formatDate(end) : ''}`,
26
26
  ]
27
27
  }
28
28
  }
@@ -1,4 +1,6 @@
1
1
  export * from './lib/feature-search.module'
2
+ export * from './lib/geocoding/geocoding.service'
3
+ export * from './lib/location-search/location-search.component'
2
4
  export * from './lib/filter-geometry.token'
3
5
  export * from './lib/record-url.token'
4
6
  export * from './lib/state/actions'
@@ -1,11 +1,19 @@
1
1
  @if (fieldType === 'dateRange') {
2
2
  <gn-ui-date-range-dropdown
3
3
  [title]="title"
4
- [startDate]="(selectedDateRange$ | async)?.start"
5
- [endDate]="(selectedDateRange$ | async)?.end"
6
- (startDateChange)="onStartDateChange($event)"
7
- (endDateChange)="onEndDateChange($event)"
4
+ [dateRange]="(selectedDateRange$ | async) ?? {}"
5
+ (dateRangeChange)="onDateRangeChange($event)"
6
+ [attr.data-cy-field]="fieldName"
8
7
  ></gn-ui-date-range-dropdown>
8
+ } @else if (fieldType === 'spatialExtent') {
9
+ <gn-ui-spatial-extent-dropdown
10
+ [title]="title"
11
+ [initialBbox]="(selectedBoundingBox$ | async) ?? null"
12
+ [maxFileSizeMb]="spatialExtentMaxFileSize"
13
+ (bboxChange)="onBboxChange($event)"
14
+ (errorChange)="onSpatialExtentError($event)"
15
+ [attr.data-cy-field]="fieldName"
16
+ ></gn-ui-spatial-extent-dropdown>
9
17
  } @else {
10
18
  <gn-ui-dropdown-multiselect
11
19
  class="w-full"
@@ -9,6 +9,8 @@ import {
9
9
  Choice,
10
10
  DateRangeDropdownComponent,
11
11
  DropdownMultiselectComponent,
12
+ SpatialExtentDropdownComponent,
13
+ SpatialExtentDropdownError,
12
14
  } from '../../../../../../libs/ui/inputs/src'
13
15
  import { Observable, of, switchMap } from 'rxjs'
14
16
  import { catchError, filter, map, startWith } from 'rxjs/operators'
@@ -22,6 +24,10 @@ import {
22
24
  } from '../utils/service/fields'
23
25
  import { DateRange } from '../../../../../../libs/api/repository/src'
24
26
  import { CommonModule } from '@angular/common'
27
+ import { BoundingBox } from '../../../../../../libs/util/shared/src'
28
+ import { NotificationsService } from '../../../../../../libs/feature/notifications/src'
29
+ import { TranslateService } from '@ngx-translate/core'
30
+ import { getOptionalSearchConfig } from '../../../../../../libs/util/app-config/src'
25
31
 
26
32
  @Component({
27
33
  selector: 'gn-ui-filter-dropdown',
@@ -33,18 +39,23 @@ import { CommonModule } from '@angular/common'
33
39
  CommonModule,
34
40
  DateRangeDropdownComponent,
35
41
  DropdownMultiselectComponent,
42
+ SpatialExtentDropdownComponent,
36
43
  ],
37
44
  })
38
45
  export class FilterDropdownComponent implements OnInit {
39
46
  private searchFacade = inject(SearchFacade)
40
47
  private searchService = inject(SearchService)
41
48
  private fieldsService = inject(FieldsService)
49
+ private notificationsService = inject(NotificationsService)
50
+ private translateService = inject(TranslateService)
42
51
 
43
52
  @Input() fieldName: string
44
53
  @Input() title: string
45
54
 
55
+ spatialExtentMaxFileSize =
56
+ getOptionalSearchConfig()?.SPATIAL_EXTENT_MAX_FILE_SIZE
57
+
46
58
  fieldType: FieldType
47
- dateRange: DateRange
48
59
  choices$: Observable<Choice[]>
49
60
  selected$ = this.searchFacade.searchFilters$.pipe(
50
61
  switchMap((filters) =>
@@ -57,15 +68,54 @@ export class FilterDropdownComponent implements OnInit {
57
68
  ) as Observable<FieldValue[]>
58
69
 
59
70
  selectedDateRange$ = this.selected$.pipe(
60
- map((selectedDateRange) => selectedDateRange as DateRange)
71
+ map((selected) => (Array.isArray(selected) ? {} : (selected as DateRange)))
61
72
  ) as Observable<DateRange>
62
73
 
74
+ selectedBoundingBox$ = this.selected$.pipe(
75
+ map((selected) =>
76
+ Array.isArray(selected) && selected.length > 0
77
+ ? (selected as BoundingBox)
78
+ : null
79
+ )
80
+ ) as Observable<BoundingBox | null>
81
+
63
82
  onSelectedValues(values: unknown[]) {
64
83
  this.fieldsService
65
84
  .buildFiltersFromFieldValues({ [this.fieldName]: values as FieldValue[] })
66
85
  .subscribe((filters) => this.searchService.updateFilters(filters))
67
86
  }
68
87
 
88
+ private spatialExtentErrorNotificationId: number | null = null
89
+
90
+ onBboxChange(bbox: BoundingBox | null) {
91
+ this.fieldsService
92
+ .buildFiltersFromFieldValues({
93
+ [this.fieldName]: bbox,
94
+ })
95
+ .subscribe((filters) => this.searchService.updateFilters(filters))
96
+ this.clearSpatialExtentErrorNotification()
97
+ }
98
+
99
+ onSpatialExtentError(error: SpatialExtentDropdownError) {
100
+ this.clearSpatialExtentErrorNotification()
101
+ this.spatialExtentErrorNotificationId =
102
+ this.notificationsService.showNotification({
103
+ type: 'error',
104
+ title: this.translateService.instant(
105
+ 'search.filters.spatialExtent.error.title'
106
+ ),
107
+ text: this.translateService.instant(error.key, error.params),
108
+ })
109
+ }
110
+
111
+ private clearSpatialExtentErrorNotification() {
112
+ if (this.spatialExtentErrorNotificationId === null) return
113
+ this.notificationsService.removeNotificationById(
114
+ this.spatialExtentErrorNotificationId
115
+ )
116
+ this.spatialExtentErrorNotificationId = null
117
+ }
118
+
69
119
  ngOnInit() {
70
120
  this.fieldType = this.fieldsService.getFieldType(this.fieldName)
71
121
  this.choices$ = this.fieldsService.getAvailableValues(this.fieldName).pipe(
@@ -80,22 +130,11 @@ export class FilterDropdownComponent implements OnInit {
80
130
  )
81
131
  }
82
132
 
83
- onStartDateChange(start: Date) {
84
- if (!start) return
85
- this.dateRange = { ...this.dateRange, start }
86
- }
87
-
88
- onEndDateChange(end: Date) {
89
- if (!end) return
90
- this.dateRange = { ...this.dateRange, end }
91
- if (this.dateRange.start && this.dateRange.end) {
92
- this.fieldsService
93
- .buildFiltersFromFieldValues({
94
- [this.fieldName]: this.dateRange,
95
- })
96
- .subscribe((filters) => {
97
- return this.searchService.updateFilters(filters)
98
- })
99
- }
133
+ onDateRangeChange(dateRange: DateRange) {
134
+ this.fieldsService
135
+ .buildFiltersFromFieldValues({
136
+ [this.fieldName]: dateRange,
137
+ })
138
+ .subscribe((filters) => this.searchService.updateFilters(filters))
100
139
  }
101
140
  }
@@ -20,7 +20,11 @@ export type GeocodingProvider =
20
20
  | DataGouvFrGeocodingProvider
21
21
 
22
22
  export const GEOCODING_PROVIDER = new InjectionToken<GeocodingProvider>(
23
- 'geocoding-provider'
23
+ 'geocoding-provider',
24
+ {
25
+ providedIn: 'root',
26
+ factory: () => ['geonames', { maxRows: 5 }] as GeocodingProvider,
27
+ }
24
28
  )
25
29
 
26
30
  @Injectable({
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { Component, Input, OnInit, inject } from '@angular/core'
2
- import { CommonModule, DatePipe } from '@angular/common'
2
+ import { CommonModule } from '@angular/common'
3
3
  import {
4
4
  catchError,
5
5
  firstValueFrom,
@@ -15,11 +15,14 @@ import { FieldType, FieldValue } from '../utils/service/fields'
15
15
  import { SearchFacade } from '../state/search.facade'
16
16
  import { SearchService } from '../utils/service/search.service'
17
17
  import { FieldsService } from '../utils/service/fields.service'
18
- import { formatUserInfo } from '../../../../../../libs/util/shared/src'
18
+ import { DateService, formatUserInfo } from '../../../../../../libs/util/shared/src'
19
19
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
20
20
 
21
21
  marker('search.filters.summaryLabel.user')
22
22
  marker('search.filters.summaryLabel.changeDate')
23
+ marker('search.filters.summaryLabel.resourceCreationRevisionDate')
24
+
25
+ const OPEN_BOUND = '…'
23
26
 
24
27
  interface DisplayedValue {
25
28
  label: string
@@ -32,13 +35,12 @@ interface DisplayedValue {
32
35
  imports: [CommonModule, BadgeComponent],
33
36
  templateUrl: './search-filters-summary-item.component.html',
34
37
  styleUrls: ['./search-filters-summary-item.component.css'],
35
- providers: [DatePipe],
36
38
  })
37
39
  export class SearchFiltersSummaryItemComponent implements OnInit {
38
40
  private searchFacade = inject(SearchFacade)
39
41
  private searchService = inject(SearchService)
40
42
  private fieldsService = inject(FieldsService)
41
- private datePipe = inject(DatePipe)
43
+ private dateService = inject(DateService)
42
44
  private translate = inject(TranslateService)
43
45
 
44
46
  @Input() fieldName: string
@@ -82,12 +84,10 @@ export class SearchFiltersSummaryItemComponent implements OnInit {
82
84
  getReadableValues(fieldValues: FieldValue[] | DateRange[]): DisplayedValue[] {
83
85
  return fieldValues.map((value) => {
84
86
  if (this.fieldType === 'dateRange') {
87
+ const { start, end } = value as DateRange
85
88
  return {
86
89
  value,
87
- label: `${this.datePipe.transform(
88
- value.start,
89
- 'dd.MM.yyyy'
90
- )} - ${this.datePipe.transform(value.end, 'dd.MM.yyyy')}`,
90
+ label: `${this.formatBound(start)} - ${this.formatBound(end)}`,
91
91
  }
92
92
  } else if (this.fieldName === 'user') {
93
93
  return { value, label: formatUserInfo(value) }
@@ -97,6 +97,16 @@ export class SearchFiltersSummaryItemComponent implements OnInit {
97
97
  })
98
98
  }
99
99
 
100
+ private formatBound(date: Date | undefined): string {
101
+ return date
102
+ ? this.dateService.formatDate(date, {
103
+ day: '2-digit',
104
+ month: '2-digit',
105
+ year: 'numeric',
106
+ })
107
+ : OPEN_BOUND
108
+ }
109
+
100
110
  async removeFilterValue(fieldValue: FieldValue | DateRange) {
101
111
  const currentFieldValues: DisplayedValue[] = await firstValueFrom(
102
112
  this.fieldValues$
@@ -2,6 +2,7 @@ import { Injectable, Injector, inject } from '@angular/core'
2
2
  import {
3
3
  AbstractSearchField,
4
4
  AvailableServicesField,
5
+ BoundingBoxSearchField,
5
6
  DateRangeSearchField,
6
7
  FieldValue,
7
8
  FullTextSearchField,
@@ -10,6 +11,7 @@ import {
10
11
  MultilingualSearchField,
11
12
  OrganizationSearchField,
12
13
  OwnerSearchField,
14
+ ResourceCreationRevisionDateSearchField,
13
15
  ResourceTypeLegacyField,
14
16
  SimpleSearchField,
15
17
  TranslatedSearchField,
@@ -42,7 +44,8 @@ marker('search.filters.producerOrg')
42
44
  marker('search.filters.publisherOrg')
43
45
  marker('search.filters.user')
44
46
  marker('search.filters.changeDate')
45
-
47
+ marker('search.filters.resourceCreationRevisionDate')
48
+ marker('search.filters.spatialExtent')
46
49
  @Injectable({
47
50
  providedIn: 'root',
48
51
  })
@@ -94,7 +97,12 @@ export class FieldsService {
94
97
  ),
95
98
  user: new UserSearchField(this.injector),
96
99
  changeDate: new DateRangeSearchField('changeDate', this.injector, 'desc'),
100
+ resourceCreationRevisionDate: new ResourceCreationRevisionDateSearchField(
101
+ this.injector,
102
+ 'desc'
103
+ ),
97
104
  availableServices: new AvailableServicesField(this.injector),
105
+ spatialExtent: new BoundingBoxSearchField('spatialExtent', this.injector),
98
106
  } as Record<string, AbstractSearchField>
99
107
 
100
108
  get supportedFields() {
@@ -20,10 +20,14 @@ import {
20
20
  isDateRange,
21
21
  METADATA_LANGUAGE,
22
22
  } from '../../../../../../../libs/api/repository/src'
23
- import { formatUserInfo } from '../../../../../../../libs/util/shared/src'
23
+ import {
24
+ BoundingBox,
25
+ formatUserInfo,
26
+ isBoundingBox,
27
+ } from '../../../../../../../libs/util/shared/src'
24
28
  import { PossibleResourceTypes } from '../../../../../../../libs/api/metadata-converter/src'
25
29
 
26
- export type FieldType = 'values' | 'dateRange'
30
+ export type FieldType = 'values' | 'dateRange' | 'spatialExtent'
27
31
 
28
32
  export type FieldValue = string | number
29
33
  export interface FieldAvailableValue {
@@ -431,6 +435,61 @@ export class DateRangeSearchField extends SimpleSearchField {
431
435
  }
432
436
  }
433
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
440
+ export class ResourceCreationRevisionDateSearchField extends DateRangeSearchField {
441
+ constructor(injector: Injector, order: 'asc' | 'desc' = 'desc') {
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
+
455
+ this.esService.registerRuntimeField(
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]); }`,
467
+ 'date'
468
+ )
469
+ }
470
+ }
471
+
472
+ export class BoundingBoxSearchField extends SimpleSearchField {
473
+ getAvailableValues(): Observable<FieldAvailableValue[]> {
474
+ return of([])
475
+ }
476
+
477
+ getFiltersForValues(values: FieldValue[]): Observable<FieldFilters> {
478
+ return of({
479
+ [this.esFieldName]: values.map(Number) as BoundingBox,
480
+ })
481
+ }
482
+
483
+ getValuesForFilter(filters: FieldFilters): Observable<FieldValue[]> {
484
+ const filter = filters[this.esFieldName]
485
+ return of(isBoundingBox(filter) ? filter : [])
486
+ }
487
+
488
+ getType(): FieldType {
489
+ return 'spatialExtent'
490
+ }
491
+ }
492
+
434
493
  marker('search.filters.availableServices.view')
435
494
  marker('search.filters.availableServices.download')
436
495
 
@@ -6,33 +6,32 @@
6
6
  matSort
7
7
  (matSortChange)="setSort($event)"
8
8
  [matSortDisableClear]="true"
9
- *ngrxLet="properties$ as properties"
10
9
  >
11
- @for (attr of _featureAttributes; track attr) {
12
- <ng-container [matColumnDef]="attr.value">
10
+ @for (column of columns; track column) {
11
+ <ng-container [matColumnDef]="column.name">
13
12
  <th
14
13
  mat-header-cell
15
14
  *matHeaderCellDef
16
15
  mat-sort-header
17
16
  class="text-sm text-black bg-white"
18
17
  >
19
- {{ attr.label }}
18
+ {{ column.label }}
20
19
  </th>
21
20
  <td
22
21
  mat-cell
23
22
  *matCellDef="let element"
24
23
  class="whitespace-nowrap pr-1 truncate"
25
24
  >
26
- {{ element[attr.value] }}
25
+ {{ element[column.name] }}
27
26
  </td>
28
27
  </ng-container>
29
28
  }
30
29
 
31
- <tr mat-header-row *matHeaderRowDef="properties; sticky: true"></tr>
30
+ <tr mat-header-row *matHeaderRowDef="columnNames; sticky: true"></tr>
32
31
  <tr
33
32
  [id]="getRowEltId(row.id)"
34
33
  mat-row
35
- *matRowDef="let row; columns: properties"
34
+ *matRowDef="let row; columns: columnNames"
36
35
  (click)="selected.emit(row)"
37
36
  [class.active]="
38
37
  activeId !== undefined && activeId !== null && row.id === activeId
@@ -6,14 +6,14 @@ import {
6
6
  Component,
7
7
  ElementRef,
8
8
  EventEmitter,
9
+ inject,
9
10
  Input,
10
11
  OnChanges,
11
12
  OnInit,
12
13
  Output,
13
14
  ViewChild,
14
- inject,
15
15
  } from '@angular/core'
16
- import { MatSort, MatSortModule } from '@angular/material/sort'
16
+ import { MatSort, MatSortModule, Sort } from '@angular/material/sort'
17
17
  import { MatTableModule } from '@angular/material/table'
18
18
  import {
19
19
  TranslateDirective,
@@ -29,12 +29,11 @@ import {
29
29
  } from '@angular/material/paginator'
30
30
  import { CustomMatPaginatorIntl } from './custom.mat.paginator.intl'
31
31
  import { CommonModule } from '@angular/common'
32
- import { BehaviorSubject, filter, firstValueFrom } from 'rxjs'
32
+ import { BehaviorSubject } from 'rxjs'
33
33
  import {
34
34
  LoadingMaskComponent,
35
35
  PopupAlertComponent,
36
36
  } from '../../../../../../libs/ui/widgets/src'
37
- import { LetDirective } from '@ngrx/component'
38
37
 
39
38
  const rowIdPrefix = 'table-item-'
40
39
 
@@ -46,6 +45,11 @@ export interface TableItemModel {
46
45
  [key: string]: TableItemType
47
46
  }
48
47
 
48
+ interface TableColumn {
49
+ name: string
50
+ label: string
51
+ }
52
+
49
53
  @Component({
50
54
  standalone: true,
51
55
  imports: [
@@ -56,7 +60,6 @@ export interface TableItemModel {
56
60
  CommonModule,
57
61
  LoadingMaskComponent,
58
62
  PopupAlertComponent,
59
- LetDirective,
60
63
  TranslatePipe,
61
64
  TranslateDirective,
62
65
  ],
@@ -71,15 +74,21 @@ export class DataTableComponent implements OnInit, AfterViewInit, OnChanges {
71
74
  private cdr = inject(ChangeDetectorRef)
72
75
  private translateService = inject(TranslateService)
73
76
 
74
- _featureAttributes = []
77
+ columnsFromFeatureCatalog: TableColumn[] = null
75
78
  @Input() set featureAttributes(value: { value: string; label: string }[]) {
76
- this._featureAttributes = value
77
- this.properties$.next(value.map((attr) => attr.value))
79
+ this.columnsFromFeatureCatalog = value.map((attrs) => ({
80
+ name: attrs.value,
81
+ label: attrs.label,
82
+ }))
78
83
  }
84
+ columnsFromDataset: TableColumn[] = []
79
85
  @Input() set dataset(value: BaseReader) {
80
86
  this.dataset_ = value
81
87
  this.dataset_.load()
82
- this.dataset_.info.then((info) => (this.count = info.itemsCount))
88
+ this.dataset_.info.then((info) => {
89
+ this.count = info.itemsCount
90
+ this.cdr.detectChanges()
91
+ })
83
92
  }
84
93
  @Input() activeId: TableItemId
85
94
  @Output() selected = new EventEmitter<any>()
@@ -88,13 +97,19 @@ export class DataTableComponent implements OnInit, AfterViewInit, OnChanges {
88
97
  @ViewChild(MatPaginator) paginator: MatPaginator
89
98
 
90
99
  dataset_: BaseReader
91
- properties$ = new BehaviorSubject<string[]>(null)
92
100
  dataSource: DataTableDataSource
93
101
  headerHeight: number
94
102
  count: number
95
103
  loading$ = new BehaviorSubject<boolean>(false)
96
104
  error = null
97
105
 
106
+ get columns() {
107
+ return this.columnsFromFeatureCatalog ?? this.columnsFromDataset
108
+ }
109
+ get columnNames() {
110
+ return this.columns.map((c) => c.name)
111
+ }
112
+
98
113
  ngOnInit() {
99
114
  this.dataSource = new DataTableDataSource()
100
115
  }
@@ -110,7 +125,7 @@ export class DataTableComponent implements OnInit, AfterViewInit, OnChanges {
110
125
  this.setPagination()
111
126
  }
112
127
 
113
- setSort(sort: MatSort) {
128
+ setSort(sort: Sort) {
114
129
  if (!this.dataset_) return
115
130
  if (!sort.active) {
116
131
  this.dataset_.orderBy()
@@ -132,15 +147,12 @@ export class DataTableComponent implements OnInit, AfterViewInit, OnChanges {
132
147
 
133
148
  async readData() {
134
149
  this.loading$.next(true)
135
- // wait for properties to be read
136
- const properties = await firstValueFrom(
137
- this.properties$.pipe(filter((p) => !!p))
138
- )
139
- const propsWithoutGeom = properties.filter(
140
- (p) => !p.toLowerCase().startsWith('geom')
141
- )
142
- this.dataset_.select(...propsWithoutGeom)
143
150
  try {
151
+ // wait for properties to be read
152
+ if (!this.columnsFromFeatureCatalog) {
153
+ this.columnsFromDataset = await this.dataset_.properties
154
+ }
155
+ this.dataset_.select(...this.columnNames)
144
156
  await this.dataSource.showData(this.dataset_.read())
145
157
  this.error = null
146
158
  } catch (error) {
@@ -25,8 +25,8 @@ export const tableItemsFixture = {
25
25
  geometry: null,
26
26
  properties: {
27
27
  id: '0003',
28
- firstName: 'Claude',
29
- lastName: 'François',
28
+ firstName: 'Lady',
29
+ lastName: 'Gaga',
30
30
  },
31
31
  },
32
32
  ] as DataItem[],
@@ -128,7 +128,8 @@ export class ServiceCapabilitiesComponent implements OnInit {
128
128
  this.loading = true
129
129
  this.availableLayers = await getLayers(
130
130
  this.apiLinks[0].url.href,
131
- this.apiLinks[0].accessServiceProtocol
131
+ this.apiLinks[0].accessServiceProtocol,
132
+ true
132
133
  )
133
134
  this.loading = false
134
135
  this.cdr.detectChanges()
@@ -4,6 +4,7 @@ export * from './lib/button/button.component'
4
4
  export * from './lib/check-toggle/check-toggle.component'
5
5
  export * from './lib/checkbox/checkbox.component'
6
6
  export * from './lib/copy-text-button/copy-text-button.component'
7
+ export * from './lib/date-adapter.providers'
7
8
  export * from './lib/date-picker/date-picker.component'
8
9
  export * from './lib/date-range-dropdown/date-range-dropdown.component'
9
10
  export * from './lib/date-range-picker/date-range-picker.component'
@@ -25,3 +26,4 @@ export * from './lib/url-input/url-input.component'
25
26
  export * from './lib/viewport-intersector/viewport-intersector.component'
26
27
  export * from './lib/search-feature-catalog/search-feature-catalog.component'
27
28
  export * from './lib/date-range-inputs/date-range-inputs.component'
29
+ export * from './lib/spatial-extent-dropdown/spatial-extent-dropdown.component'