geonetwork-ui 2.11.0-dev.2478042b2 → 2.11.0-dev.30db8c2d6

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 (89) hide show
  1. package/fesm2022/geonetwork-ui.mjs +2052 -1468
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +303 -124
  4. package/index.d.ts.map +1 -1
  5. package/package.json +7 -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 +134 -35
  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 +24 -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/add-layer-from-file/add-layer-from-file.component.html +1 -0
  24. package/src/libs/feature/map/src/lib/feature-map.module.ts +1 -8
  25. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  26. package/src/libs/feature/notify-reuse/src/lib/notify-reuse-form/notify-reuse-form.component.ts +8 -3
  27. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +3 -1
  28. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -1
  29. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  30. package/src/libs/feature/search/src/index.ts +2 -0
  31. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +18 -4
  32. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +80 -19
  33. package/src/libs/feature/search/src/lib/geocoding/geocoding.service.ts +105 -0
  34. package/src/libs/feature/search/src/lib/location-search/location-search.component.html +18 -0
  35. package/src/libs/feature/search/src/lib/location-search/location-search.component.ts +114 -0
  36. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +19 -8
  37. package/src/libs/feature/search/src/lib/state/reducer.ts +1 -4
  38. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +64 -4
  39. package/src/libs/feature/search/src/lib/utils/service/fields.ts +92 -9
  40. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.html +2 -2
  41. package/src/libs/ui/elements/src/lib/record-feature-catalog/feature-catalog-list/feature-catalog-list.component.ts +2 -2
  42. package/src/libs/ui/elements/src/lib/service-capabilities/service-capabilities.component.ts +2 -1
  43. package/src/libs/ui/inputs/src/index.ts +2 -0
  44. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.css +0 -4
  45. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.html +12 -3
  46. package/src/libs/ui/inputs/src/lib/autocomplete/autocomplete.component.ts +11 -3
  47. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  48. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  49. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  50. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  51. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  52. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  53. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  54. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  55. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  56. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +7 -4
  57. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.ts +12 -9
  58. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.model.ts +2 -2
  59. package/src/libs/ui/inputs/src/lib/search-feature-catalog/search-feature-catalog.component.ts +2 -1
  60. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +117 -0
  61. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +244 -0
  62. package/src/libs/ui/layout/src/lib/block-list/block-list.component.css +1 -0
  63. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  64. package/src/libs/util/app-config/src/lib/app-config.ts +37 -0
  65. package/src/libs/util/app-config/src/lib/fixtures.ts +37 -1
  66. package/src/libs/util/app-config/src/lib/model.ts +25 -1
  67. package/src/libs/util/shared/src/index.ts +1 -0
  68. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  69. package/src/libs/util/shared/src/lib/links/link-utils.ts +19 -10
  70. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  71. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  72. package/src/libs/util/shared/src/lib/utils/geojson.ts +24 -0
  73. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  74. package/tailwind.base.css +5 -0
  75. package/translations/de.json +24 -4
  76. package/translations/en.json +23 -3
  77. package/translations/es.json +22 -2
  78. package/translations/fr.json +28 -8
  79. package/translations/it.json +22 -2
  80. package/translations/nl.json +22 -2
  81. package/translations/pt.json +22 -2
  82. package/translations/sk.json +22 -2
  83. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.html +0 -41
  84. package/src/libs/feature/map/src/lib/geocoding/geocoding.component.ts +0 -112
  85. package/src/libs/feature/map/src/lib/geocoding.service.ts +0 -57
  86. package/src/libs/feature/map/src/lib/style/index.ts +0 -2
  87. package/src/libs/feature/map/src/lib/style/map-style.fixtures.ts +0 -47
  88. package/src/libs/feature/map/src/lib/style/map-style.service.ts +0 -108
  89. /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,30 +1,149 @@
1
1
  <gn-ui-button
2
2
  type="outline"
3
3
  extraClass="bg-background w-full !p-[8px] !pl-[16px]"
4
- (buttonClick)="picker.open()"
4
+ [title]="title"
5
+ (buttonClick)="openOverlay()"
6
+ cdkOverlayOrigin
7
+ #overlayOrigin="cdkOverlayOrigin"
5
8
  >
6
9
  <div class="grow flex items-center mr-2 gap-2 overflow-hidden">
7
10
  <div class="text-left font-medium truncate py-1">
8
11
  {{ title }}
9
12
  </div>
13
+ @if (selectedDatesCount) {
14
+ <div
15
+ class="gn-ui-multiselect-counter shrink-0 rounded-full font-bold text-[12px] w-5 h-5 flex items-center justify-center mr-1 selected-count"
16
+ >
17
+ {{ selectedDatesCount }}
18
+ </div>
19
+ }
10
20
  </div>
21
+ <button
22
+ class="h-6 w-6 flex items-center justify-center"
23
+ data-test="dropdown-clear"
24
+ >
25
+ @if (selectedDatesCount && !overlayOpen) {
26
+ <ng-icon
27
+ class="shrink-0 opacity-40 hover:opacity-80 transition-colors clear-btn"
28
+ (click)="clearDates($event)"
29
+ name="matClose"
30
+ ></ng-icon>
31
+ }
32
+ </button>
11
33
  <ng-icon
12
- [name]="isPickerDisplayed ? 'matExpandLess' : 'matExpandMore'"
34
+ data-test="dropdown-toggle"
35
+ [name]="overlayOpen ? 'matExpandLess' : 'matExpandMore'"
13
36
  class="shrink-0 opacity-40"
14
37
  >
15
38
  </ng-icon>
16
39
  </gn-ui-button>
17
- <!--date range input is just present for output events and hidden from the DOM-->
18
- <mat-date-range-input [rangePicker]="picker">
19
- <input
20
- matStartDate
21
- [value]="startDate"
22
- (dateInput)="startDateChange.emit($event.value)"
23
- />
24
- <input
25
- matEndDate
26
- [value]="endDate"
27
- (dateInput)="endDateChange.emit($event.value)"
28
- />
29
- </mat-date-range-input>
30
- <mat-date-range-picker #picker></mat-date-range-picker>
40
+
41
+ <ng-template
42
+ cdkConnectedOverlay
43
+ cdkConnectedOverlayHasBackdrop
44
+ cdkConnectedOverlayBackdropClass="cdk-overlay-transparent-backdrop"
45
+ [cdkConnectedOverlayOrigin]="overlayOrigin"
46
+ [cdkConnectedOverlayOpen]="overlayOpen"
47
+ [cdkConnectedOverlayPositions]="overlayPositions"
48
+ [cdkConnectedOverlayScrollStrategy]="scrollStrategy"
49
+ (overlayOutsideClick)="closeOverlay()"
50
+ (detach)="closeOverlay()"
51
+ >
52
+ <div
53
+ class="w-[296px] flex flex-col gap-2 bg-white border border-gray-400 rounded px-4 py-3 shadow-lg text-main text-[16px] leading-[24px]"
54
+ data-test="date-range-panel"
55
+ >
56
+ <div class="flex flex-col justify-center border-b border-gray-300 pb-1">
57
+ <ng-container
58
+ [ngTemplateOutlet]="boundRow"
59
+ [ngTemplateOutletContext]="{
60
+ bound: 'start',
61
+ label: 'daterange.filter.from',
62
+ date: startDate,
63
+ }"
64
+ ></ng-container>
65
+ @if (expandedBound === 'start') {
66
+ <div class="calendar" animate.enter="expand" animate.leave="collapse">
67
+ <mat-calendar
68
+ data-test="start-date-calendar"
69
+ [selected]="startDate"
70
+ [startAt]="startDate ?? endDate"
71
+ [maxDate]="endDate"
72
+ (selectedChange)="setStartDate($event)"
73
+ ></mat-calendar>
74
+ </div>
75
+ }
76
+ </div>
77
+ <div class="flex flex-col justify-center">
78
+ <ng-container
79
+ [ngTemplateOutlet]="boundRow"
80
+ [ngTemplateOutletContext]="{
81
+ bound: 'end',
82
+ label: 'daterange.filter.to',
83
+ date: endDate,
84
+ }"
85
+ ></ng-container>
86
+ @if (expandedBound === 'end') {
87
+ <div class="calendar" animate.enter="expand" animate.leave="collapse">
88
+ <mat-calendar
89
+ data-test="end-date-calendar"
90
+ [selected]="endDate"
91
+ [startAt]="endDate ?? startDate"
92
+ [minDate]="startDate"
93
+ (selectedChange)="setEndDate($event)"
94
+ ></mat-calendar>
95
+ </div>
96
+ }
97
+ </div>
98
+ </div>
99
+ </ng-template>
100
+
101
+ <ng-template #boundRow let-bound="bound" let-label="label" let-date="date">
102
+ <div class="flex items-center gap-1 w-full">
103
+ <span class="w-[80px] shrink-0" [translate]="label"></span>
104
+ <div
105
+ class="flex-1 min-w-0 flex items-center gap-1 border rounded-lg px-2 py-1 transition-colors"
106
+ [class]="
107
+ invalidBounds[bound]
108
+ ? 'border-red-400 text-red-800 focus-within:border-red-800 hover:border-red-800'
109
+ : 'border-gray-400 focus-within:border-main hover:border-main'
110
+ "
111
+ >
112
+ <ng-icon
113
+ name="iconoirCalendar"
114
+ class="shrink-0"
115
+ aria-hidden="true"
116
+ ></ng-icon>
117
+ <input
118
+ type="text"
119
+ inputmode="numeric"
120
+ autocomplete="off"
121
+ [gnUiAutofocus]="bound === expandedBound"
122
+ class="min-w-0 grow bg-transparent focus:outline-none"
123
+ [value]="date ? formatDate(date) : ''"
124
+ [placeholder]="'daterange.filter.datePlaceholder' | translate"
125
+ [attr.aria-label]="label | translate"
126
+ [attr.aria-invalid]="invalidBounds[bound] || null"
127
+ [attr.data-test]="bound + '-date-input'"
128
+ (change)="onDateInput(bound, $event)"
129
+ (blur)="normalizeDateInput(bound, $event)"
130
+ (click)="expandedBound !== bound && toggleBound(bound)"
131
+ />
132
+ </div>
133
+ <div class="shrink-0 flex items-center justify-end">
134
+ <gn-ui-button
135
+ type="light"
136
+ [style.--gn-ui-button-padding]="0"
137
+ [style.--gn-ui-button-width]="'24px'"
138
+ [style.--gn-ui-button-height]="'24px'"
139
+ [attr.aria-expanded]="expandedBound === bound"
140
+ [attr.data-test]="bound + '-date-toggle'"
141
+ (buttonClick)="toggleBound(bound)"
142
+ >
143
+ <ng-icon
144
+ [name]="expandedBound === bound ? 'matExpandLess' : 'matExpandMore'"
145
+ ></ng-icon>
146
+ </gn-ui-button>
147
+ </div>
148
+ </div>
149
+ </ng-template>
@@ -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
  }
@@ -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
  )