geonetwork-ui 2.11.0-dev.96d0c6ce2 → 2.11.0-dev.9bd41a961

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 (95) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1506 -938
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +295 -127
  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/metadata-converter/src/lib/gn4/types/elasticsearch.model.ts +13 -1
  8. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +130 -37
  9. package/src/libs/api/repository/src/lib/gn4/gn4-repository.ts +40 -16
  10. package/src/libs/common/domain/src/lib/model/record/metadata.model.ts +12 -0
  11. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +20 -1
  12. package/src/libs/common/domain/src/lib/model/search/sort-by.model.ts +1 -5
  13. package/src/libs/common/domain/src/lib/repository/records-repository.interface.ts +2 -2
  14. package/src/libs/feature/dataviz/src/lib/chart-view/chart-view.component.ts +3 -2
  15. package/src/libs/feature/dataviz/src/lib/geo-table-view/geo-table-view.component.ts +2 -1
  16. package/src/libs/feature/dataviz/src/lib/service/data.service.ts +22 -22
  17. package/src/libs/feature/map/src/index.ts +0 -1
  18. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  19. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  20. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  21. package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
  22. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
  23. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
  24. package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
  25. package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
  26. package/src/libs/feature/router/src/lib/default/router.service.ts +1 -1
  27. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  28. package/src/libs/feature/search/src/index.ts +2 -0
  29. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +12 -4
  30. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +58 -19
  31. package/src/libs/feature/{map/src/lib → search/src/lib/geocoding}/geocoding.service.ts +5 -1
  32. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +8 -0
  33. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +40 -0
  34. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +18 -8
  35. package/src/libs/feature/search/src/lib/sort-by/sort-by.component.ts +1 -1
  36. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +9 -1
  37. package/src/libs/feature/search/src/lib/utils/service/fields.ts +61 -2
  38. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  39. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  40. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  41. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  42. package/src/libs/ui/inputs/src/index.ts +2 -0
  43. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +10 -1
  44. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +8 -0
  45. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  46. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  47. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  48. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  49. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  50. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  51. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  52. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  53. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  54. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  55. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  56. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  57. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
  58. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -0
  59. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  60. package/src/libs/util/app-config/src/lib/app-config.ts +3 -0
  61. package/src/libs/util/app-config/src/lib/model.ts +1 -0
  62. package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
  63. package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
  64. package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
  65. package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
  66. package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
  67. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
  68. package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
  69. package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
  70. package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
  71. package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
  72. package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
  73. package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
  74. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
  75. package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
  76. package/src/libs/util/shared/src/index.ts +1 -0
  77. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  78. package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
  79. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  80. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  81. package/src/libs/util/shared/src/lib/utils/geojson.ts +8 -0
  82. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  83. package/tailwind.base.css +5 -0
  84. package/translations/de.json +18 -4
  85. package/translations/en.json +17 -3
  86. package/translations/es.json +15 -1
  87. package/translations/fr.json +18 -4
  88. package/translations/it.json +16 -2
  89. package/translations/nl.json +15 -1
  90. package/translations/pt.json +15 -1
  91. package/translations/sk.json +16 -2
  92. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  93. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  94. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
  95. /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
@@ -1,64 +1,201 @@
1
1
  import {
2
- AfterViewChecked,
2
+ ChangeDetectionStrategy,
3
3
  ChangeDetectorRef,
4
4
  Component,
5
- ElementRef,
6
5
  EventEmitter,
7
6
  Input,
8
7
  Output,
9
8
  ViewChild,
10
9
  inject,
11
10
  } from '@angular/core'
12
-
13
- import { MatNativeDateModule } from '@angular/material/core'
11
+ import { NgTemplateOutlet } from '@angular/common'
12
+ import {
13
+ CdkOverlayOrigin,
14
+ ConnectedPosition,
15
+ OverlayModule,
16
+ ScrollStrategyOptions,
17
+ } from '@angular/cdk/overlay'
18
+ import {
19
+ DateAdapter,
20
+ MAT_DATE_FORMATS,
21
+ MatDateFormats,
22
+ } from '@angular/material/core'
14
23
  import { MatDatepickerModule } from '@angular/material/datepicker'
24
+ import { type Locale } from 'date-fns/locale'
25
+ import { NgIcon, provideIcons, provideNgIconsConfig } from '@ng-icons/core'
26
+ import {
27
+ matClose,
28
+ matExpandLess,
29
+ matExpandMore,
30
+ } from '@ng-icons/material-icons/baseline'
31
+ import { iconoirCalendar } from '@ng-icons/iconoir'
32
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
33
+ import { TranslateDirective, TranslatePipe } from '@ngx-translate/core'
34
+ import { FieldFilterByRange } from '../../../../../../libs/common/domain/src/lib/model/search'
35
+ import {
36
+ AutofocusDirective,
37
+ propagateToDocumentOnly,
38
+ } from '../../../../../../libs/util/shared/src'
15
39
  import { ButtonComponent } from '../button/button.component'
16
- import { OverlayContainer } from '@angular/cdk/overlay'
17
- import { NgIconComponent, provideIcons } from '@ng-icons/core'
18
- import { matExpandLess, matExpandMore } from '@ng-icons/material-icons/baseline'
40
+ import { provideLocalizedDateAdapter } from '../date-adapter.providers'
41
+
42
+ export type DateRangeBound = 'start' | 'end'
19
43
 
20
44
  @Component({
21
45
  selector: 'gn-ui-date-range-dropdown',
22
46
  standalone: true,
23
47
  imports: [
24
- NgIconComponent,
25
- MatNativeDateModule,
26
- MatDatepickerModule,
48
+ AutofocusDirective,
27
49
  ButtonComponent,
50
+ MatDatepickerModule,
51
+ NgIcon,
52
+ NgTemplateOutlet,
53
+ OverlayModule,
54
+ TranslateDirective,
55
+ TranslatePipe,
28
56
  ],
29
57
  providers: [
30
58
  provideIcons({
31
- matExpandMore,
59
+ iconoirCalendar,
60
+ matClose,
32
61
  matExpandLess,
62
+ matExpandMore,
33
63
  }),
64
+ provideNgIconsConfig({
65
+ size: '1.5rem',
66
+ }),
67
+ provideLocalizedDateAdapter(),
34
68
  ],
35
69
  templateUrl: './date-range-dropdown.component.html',
36
70
  styleUrls: ['./date-range-dropdown.component.css'],
71
+ changeDetection: ChangeDetectionStrategy.OnPush,
37
72
  })
38
- export class DateRangeDropdownComponent implements AfterViewChecked {
39
- private overlayContainer = inject(OverlayContainer)
73
+ export class DateRangeDropdownComponent {
74
+ private scrollStrategies = inject(ScrollStrategyOptions)
75
+ private dateAdapter = inject<DateAdapter<Date, Locale>>(DateAdapter)
76
+ private dateFormats = inject<MatDateFormats>(MAT_DATE_FORMATS)
40
77
  private cdr = inject(ChangeDetectorRef)
41
78
 
42
79
  @Input() title: string
43
- @Input() startDate: Date
44
- @Input() endDate: Date
45
- @Output() startDateChange = new EventEmitter<Date>()
46
- @Output() endDateChange = new EventEmitter<Date>()
80
+ @Input() dateRange: FieldFilterByRange = {}
81
+ @Output() dateRangeChange = new EventEmitter<FieldFilterByRange>()
82
+
83
+ @ViewChild('overlayOrigin') overlayOrigin: CdkOverlayOrigin
84
+
85
+ overlayPositions: ConnectedPosition[] = [
86
+ {
87
+ originX: 'start',
88
+ originY: 'bottom',
89
+ overlayX: 'start',
90
+ overlayY: 'top',
91
+ offsetY: 8,
92
+ },
93
+ {
94
+ originX: 'start',
95
+ originY: 'top',
96
+ overlayX: 'start',
97
+ overlayY: 'bottom',
98
+ offsetY: -8,
99
+ },
100
+ ]
101
+ scrollStrategy = this.scrollStrategies.reposition()
102
+ overlayOpen = false
103
+ expandedBound: DateRangeBound | null = 'start'
104
+ invalidBounds: Record<DateRangeBound, boolean> = { start: false, end: false }
105
+
106
+ constructor() {
107
+ // redraw when the lazily loaded UI locale reaches the adapter
108
+ this.dateAdapter.localeChanges
109
+ .pipe(takeUntilDestroyed())
110
+ .subscribe(() => this.cdr.markForCheck())
111
+ }
112
+
113
+ get startDate() {
114
+ return this.dateRange.start
115
+ }
116
+
117
+ get endDate() {
118
+ return this.dateRange.end
119
+ }
47
120
 
48
- @ViewChild('picker') picker: ElementRef
49
- isPickerDisplayed = false
121
+ get selectedDatesCount() {
122
+ return (this.startDate ? 1 : 0) + (this.endDate ? 1 : 0)
123
+ }
124
+
125
+ openOverlay() {
126
+ this.expandedBound = 'start'
127
+ this.overlayOpen = true
128
+ }
129
+
130
+ closeOverlay() {
131
+ this.overlayOpen = false
132
+ }
133
+
134
+ toggleBound(bound: DateRangeBound) {
135
+ this.expandedBound = this.expandedBound === bound ? null : bound
136
+ }
137
+
138
+ formatDate(date: Date) {
139
+ return this.dateAdapter.format(date, this.dateFormats.display.dateInput)
140
+ }
141
+
142
+ onDateInput(bound: DateRangeBound, event: Event) {
143
+ const typedText = (event.target as HTMLInputElement).value.trim()
144
+ const date = typedText
145
+ ? this.dateAdapter.parse(typedText, this.dateFormats.parse.dateInput)
146
+ : null
147
+ if (!this.isAcceptable(bound, typedText, date)) {
148
+ // keep the text as typed and flag it; the filter stays on its last value
149
+ this.invalidBounds = { ...this.invalidBounds, [bound]: true }
150
+ return
151
+ }
152
+ if (bound === 'start') this.setStartDate(date)
153
+ else this.setEndDate(date)
154
+ }
155
+
156
+ normalizeDateInput(bound: DateRangeBound, event: Event) {
157
+ if (this.invalidBounds[bound]) return
158
+ const date = bound === 'start' ? this.startDate : this.endDate
159
+ const input = event.target as HTMLInputElement
160
+ input.value = date ? this.formatDate(date) : ''
161
+ }
162
+
163
+ private isAcceptable(
164
+ bound: DateRangeBound,
165
+ typedText: string,
166
+ date: Date | null
167
+ ) {
168
+ if (!typedText) return true
169
+ if (!date || !this.dateAdapter.isValid(date)) return false
170
+ return bound === 'start'
171
+ ? !this.endDate || date <= this.endDate
172
+ : !this.startDate || date >= this.startDate
173
+ }
174
+
175
+ setStartDate(date: Date) {
176
+ this.invalidBounds = { ...this.invalidBounds, start: false }
177
+ this.applyDateRange({ ...this.dateRange, start: date })
178
+ this.expandedBound = 'end'
179
+ }
180
+
181
+ setEndDate(date: Date) {
182
+ this.invalidBounds = { ...this.invalidBounds, end: false }
183
+ this.applyDateRange({ ...this.dateRange, end: date })
184
+ this.expandedBound = null
185
+ }
50
186
 
51
- ngAfterViewChecked() {
52
- this.checkPickerOverlay()
187
+ clearDates(event: Event) {
188
+ this.invalidBounds = { start: false, end: false }
189
+ this.expandedBound = 'start'
190
+ this.applyDateRange({})
191
+ propagateToDocumentOnly(event)
53
192
  }
54
193
 
55
- checkPickerOverlay() {
56
- const overlayContainerElement = this.overlayContainer.getContainerElement()
57
- setTimeout(() => {
58
- this.isPickerDisplayed =
59
- overlayContainerElement.querySelector('.mat-datepicker-content') !==
60
- null
61
- this.cdr.detectChanges()
62
- }, 200) // FIXME: find a better way to deal with animation delay
194
+ private applyDateRange(dateRange: FieldFilterByRange) {
195
+ this.dateRange = {
196
+ ...(dateRange.start && { start: dateRange.start }),
197
+ ...(dateRange.end && { end: dateRange.end }),
198
+ }
199
+ this.dateRangeChange.emit(this.dateRange)
63
200
  }
64
201
  }
@@ -5,7 +5,6 @@ import {
5
5
  Input,
6
6
  Output,
7
7
  } from '@angular/core'
8
- import { MatNativeDateModule } from '@angular/material/core'
9
8
  import { MatDatepickerModule } from '@angular/material/datepicker'
10
9
  import { ButtonComponent } from '../button/button.component'
11
10
  import {
@@ -14,6 +13,7 @@ import {
14
13
  provideNgIconsConfig,
15
14
  } from '@ng-icons/core'
16
15
  import { iconoirCalendar } from '@ng-icons/iconoir'
16
+ import { provideLocalizedDateAdapter } from '../date-adapter.providers'
17
17
 
18
18
  @Component({
19
19
  selector: 'gn-ui-date-range-picker',
@@ -21,17 +21,13 @@ import { iconoirCalendar } from '@ng-icons/iconoir'
21
21
  styleUrls: ['./date-range-picker.component.css'],
22
22
  changeDetection: ChangeDetectionStrategy.OnPush,
23
23
  standalone: true,
24
- imports: [
25
- MatNativeDateModule,
26
- MatDatepickerModule,
27
- ButtonComponent,
28
- NgIconComponent,
29
- ],
24
+ imports: [MatDatepickerModule, ButtonComponent, NgIconComponent],
30
25
  providers: [
31
26
  provideIcons({ iconoirCalendar }),
32
27
  provideNgIconsConfig({
33
28
  size: '1.5rem',
34
29
  }),
30
+ provideLocalizedDateAdapter(),
35
31
  ],
36
32
  })
37
33
  export class DateRangePickerComponent {
@@ -1,4 +1,5 @@
1
1
  ngx-dropzone {
2
2
  height: auto;
3
3
  border: none;
4
+ overflow: hidden;
4
5
  }
@@ -1,18 +1,31 @@
1
- <div class="flex h-full">
1
+ <div
2
+ class="flex h-full items-center"
3
+ [class]="extraClass"
4
+ (click)="openFileSelector()"
5
+ >
2
6
  <ngx-dropzone
3
- class="flex-1"
7
+ class="flex-1 overflow-hidden"
4
8
  [multiple]="false"
5
- (change)="selectFile($event)"
9
+ [disableClick]="true"
6
10
  [accept]="accept"
11
+ [maxFileSize]="maxFileSizeBytes"
12
+ [style.background-color]="dropzoneBackgroundColor"
13
+ (change)="selectFile($event)"
7
14
  >
8
- @if (!fileName) {
9
- <div class="text-gray-900 pl-2 py-2" translate="">
15
+ @if (!fileName || !showFileName) {
16
+ <div [class]="textClass">
10
17
  {{ placeholder }}
11
18
  </div>
12
19
  }
13
20
 
14
- @if (fileName) {
15
- <div class="text-gray-900 pl-2 py-2">{{ fileName }}</div>
21
+ @if (fileName && showFileName) {
22
+ <div [class]="textClass">
23
+ {{ fileName }}
24
+ </div>
16
25
  }
17
26
  </ngx-dropzone>
27
+
28
+ @if (icon) {
29
+ <ng-icon [name]="icon" class="shrink-0 text-gray-950"></ng-icon>
30
+ }
18
31
  </div>
@@ -1,30 +1,71 @@
1
- import { Component, EventEmitter, Input, Output } from '@angular/core'
1
+ import {
2
+ Component,
3
+ EventEmitter,
4
+ Input,
5
+ Output,
6
+ ViewChild,
7
+ } from '@angular/core'
2
8
  import { marker } from '@biesbjerg/ngx-translate-extract-marker'
3
- import { NgxDropzoneModule } from 'ngx-dropzone'
9
+ import { NgxDropzoneComponent, NgxDropzoneModule } from 'ngx-dropzone'
10
+ import { NgIcon } from '@ng-icons/core'
11
+ import { megabytesToBytes } from '../../../../../../libs/util/shared/src'
4
12
 
5
13
  export const placeholder = 'dropFile'
6
14
  marker('dropFile')
7
15
 
16
+ export type DragAndDropFileInputError = 'invalid-extension' | 'file-too-large'
17
+
8
18
  @Component({
9
19
  selector: 'gn-ui-drag-and-drop-file-input',
10
20
  templateUrl: './drag-and-drop-file-input.component.html',
11
21
  styleUrls: ['./drag-and-drop-file-input.component.css'],
12
22
  standalone: true,
13
- imports: [NgxDropzoneModule],
23
+ imports: [NgxDropzoneModule, NgIcon],
14
24
  })
15
25
  export class DragAndDropFileInputComponent {
16
26
  @Input() placeholder = placeholder
17
27
  @Input() accept = '*'
18
- @Output() fileChange = new EventEmitter<any>()
28
+ @Input() maxFileSizeMb: number | null = null
29
+ @Input() icon: string | null = null
30
+ @Input() dropzoneBackgroundColor: string | null = null
31
+ @Input() textClass = ''
32
+ @Input() extraClass = ''
33
+ @Input() showFileName = true
34
+
35
+ @Output() fileChange = new EventEmitter<File>()
36
+ @Output() errorChange = new EventEmitter<DragAndDropFileInputError>()
37
+
19
38
  selectedFile: File = null
20
39
 
40
+ @ViewChild(NgxDropzoneComponent) private dropzone: NgxDropzoneComponent
41
+
21
42
  get fileName(): string | null {
22
43
  return this.selectedFile && this.selectedFile.name
23
44
  }
24
45
 
46
+ get maxFileSizeBytes(): number | null {
47
+ return typeof this.maxFileSizeMb === 'number'
48
+ ? megabytesToBytes(this.maxFileSizeMb)
49
+ : null
50
+ }
51
+
25
52
  selectFile(event) {
53
+ if (event.rejectedFiles?.length) {
54
+ const reason = event.rejectedFiles[0].reason
55
+ this.errorChange.emit(
56
+ reason === 'size' ? 'file-too-large' : 'invalid-extension'
57
+ )
58
+ return
59
+ }
26
60
  this.selectedFile = event.addedFiles[0]
27
-
28
61
  this.fileChange.emit(this.selectedFile)
29
62
  }
63
+
64
+ openFileSelector() {
65
+ this.dropzone.showFileSelector()
66
+ }
67
+
68
+ clear() {
69
+ this.selectedFile = null
70
+ }
30
71
  }
@@ -20,10 +20,13 @@
20
20
  </div>
21
21
  }
22
22
  </div>
23
- <button class="h-6 w-6" data-cy="clearSelection">
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
  }
@@ -0,0 +1,111 @@
1
+ <gn-ui-button
2
+ type="outline"
3
+ extraClass="bg-background w-full !p-[8px] !pl-[16px]"
4
+ [title]="title"
5
+ (buttonClick)="toggleOverlay()"
6
+ cdkOverlayOrigin
7
+ #overlayOrigin="cdkOverlayOrigin"
8
+ >
9
+ <div class="grow flex items-center mr-2 gap-2 overflow-hidden">
10
+ <div class="text-left font-medium truncate py-1">
11
+ {{ title }}
12
+ </div>
13
+ @if (hasSelection) {
14
+ <div
15
+ class="bg-primary-lighter text-white shrink-0 rounded-full font-bold text-[12px] w-[22px] h-[22px] flex items-center justify-center"
16
+ >
17
+ 1
18
+ </div>
19
+ }
20
+ </div>
21
+ <button class="h-6 w-6 flex items-center" data-cy="clearSelection">
22
+ @if (hasSelection && !overlayOpen) {
23
+ <ng-icon
24
+ class="shrink-0 opacity-40 mr-1.5 hover:opacity-80 transition-colors clear-btn"
25
+ (click)="removeSelection($event)"
26
+ name="matClose"
27
+ ></ng-icon>
28
+ }
29
+ </button>
30
+ <ng-icon
31
+ [name]="overlayOpen ? 'matExpandLess' : 'matExpandMore'"
32
+ class="shrink-0 opacity-40"
33
+ >
34
+ </ng-icon>
35
+ </gn-ui-button>
36
+
37
+ <ng-template
38
+ cdkConnectedOverlay
39
+ cdkConnectedOverlayHasBackdrop
40
+ cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
41
+ [cdkConnectedOverlayOrigin]="overlayOrigin"
42
+ [cdkConnectedOverlayOpen]="overlayOpen"
43
+ [cdkConnectedOverlayPositions]="overlayPositions"
44
+ [cdkConnectedOverlayScrollStrategy]="scrollStrategy"
45
+ [cdkConnectedOverlayFlexibleDimensions]="true"
46
+ (overlayOutsideClick)="closeOverlay()"
47
+ (detach)="closeOverlay()"
48
+ >
49
+ <div
50
+ class="bg-white border border-gray-400 rounded shadow-lg pt-[12px] px-[16px] overlay-container w-[280px] flex flex-col gap-2"
51
+ [style.min-width]="overlayMinWidth"
52
+ >
53
+ <div class="text-xs text-black">
54
+ {{ 'search.filters.spatialExtent.helpText' | translate }}
55
+ </div>
56
+
57
+ <div
58
+ [class]="
59
+ (hasSelection
60
+ ? 'border-b-[1px] border-gray-400 pb-[20px]'
61
+ : 'pb-[8px]') + ' text-black'
62
+ "
63
+ >
64
+ <gn-ui-drag-and-drop-file-input
65
+ accept=".json,.geojson"
66
+ [placeholder]="'search.filters.spatialExtent.import' | translate"
67
+ [maxFileSizeMb]="maxFileSizeMb"
68
+ [showFileName]="false"
69
+ textClass="text-black truncate"
70
+ dropzoneBackgroundColor="transparent"
71
+ icon="iconoirImport"
72
+ extraClass="gn-ui-gray-outline-input w-full !h-[32px] px-[8px] py-[4px] justify-between"
73
+ (fileChange)="handleFileSelected($event)"
74
+ (errorChange)="handleFileError($event)"
75
+ data-cy="importGeojson"
76
+ ></gn-ui-drag-and-drop-file-input>
77
+ </div>
78
+
79
+ @if (hasSelection) {
80
+ <gn-ui-button
81
+ [type]="'primary-light'"
82
+ [title]="selectionDeleteLabelKey | translate: selectionLabelParams"
83
+ (buttonClick)="removeSelection($event)"
84
+ extraClass="group gap-x-2 w-full h-[28px] px-[8px] py-[4px] mb-[12px] bg-primary-lighter text-white"
85
+ data-test="spatial-extent-selected-item"
86
+ >
87
+ <ng-icon
88
+ name="iconoirSquareDashed"
89
+ class="shrink-0"
90
+ size="15px"
91
+ ></ng-icon>
92
+ <span class="group-hover:hidden truncate text-sm m-auto">
93
+ {{ selectionLabelKey | translate: selectionLabelParams }}
94
+ </span>
95
+ <span class="hidden group-hover:block truncate text-sm m-auto">
96
+ {{ selectionDeleteLabelKey | translate: selectionLabelParams }}
97
+ </span>
98
+ <ng-icon
99
+ name="iconoirCheckCircle"
100
+ class="group-hover:hidden shrink-0"
101
+ size="14px"
102
+ ></ng-icon>
103
+ <ng-icon
104
+ name="iconoirTrash"
105
+ class="!hidden group-hover:!block shrink-0"
106
+ size="15px"
107
+ ></ng-icon>
108
+ </gn-ui-button>
109
+ }
110
+ </div>
111
+ </ng-template>