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

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 (77) hide show
  1. package/fesm2022/geonetwork-ui.mjs +1244 -677
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +219 -78
  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 +48 -7
  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 +13 -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/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  16. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  17. package/src/libs/feature/record/src/lib/state/mdview.actions.ts +4 -8
  18. package/src/libs/feature/record/src/lib/state/mdview.effects.ts +7 -16
  19. package/src/libs/feature/record/src/lib/state/mdview.facade.ts +1 -3
  20. package/src/libs/feature/record/src/lib/state/mdview.reducer.ts +4 -9
  21. package/src/libs/feature/record/src/lib/state/mdview.selectors.ts +2 -7
  22. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  23. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +11 -4
  24. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +58 -19
  25. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +17 -8
  26. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +3 -1
  27. package/src/libs/feature/search/src/lib/utils/service/fields.ts +27 -2
  28. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.html +6 -7
  29. package/src/libs/ui/dataviz/src/lib/data-table/data-table.component.ts +31 -19
  30. package/src/libs/ui/dataviz/src/lib/data-table/data-table.fixtures.ts +2 -2
  31. package/src/libs/ui/inputs/src/index.ts +2 -0
  32. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  33. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  34. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  35. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  36. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  37. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  38. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  39. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  40. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  41. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +1 -1
  42. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css +0 -0
  43. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
  44. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -0
  45. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +6 -5
  46. package/src/libs/util/app-config/src/lib/app-config.ts +3 -0
  47. package/src/libs/util/app-config/src/lib/model.ts +1 -0
  48. package/src/libs/util/data-fetcher/src/lib/data-fetcher.ts +19 -22
  49. package/src/libs/util/data-fetcher/src/lib/engine/duckdb.ts +185 -0
  50. package/src/libs/util/data-fetcher/src/lib/engine/results.ts +63 -0
  51. package/src/libs/util/data-fetcher/src/lib/{sql-utils.ts → engine/sql-utils.ts} +28 -13
  52. package/src/libs/util/data-fetcher/src/lib/model.ts +2 -1
  53. package/src/libs/util/data-fetcher/src/lib/readers/base-file.ts +53 -38
  54. package/src/libs/util/data-fetcher/src/lib/readers/base.ts +11 -0
  55. package/src/libs/util/data-fetcher/src/lib/readers/csv.ts +9 -47
  56. package/src/libs/util/data-fetcher/src/lib/readers/excel.ts +27 -27
  57. package/src/libs/util/data-fetcher/src/lib/readers/geojson.ts +5 -24
  58. package/src/libs/util/data-fetcher/src/lib/readers/gml.ts +5 -49
  59. package/src/libs/util/data-fetcher/src/lib/readers/json.ts +5 -23
  60. package/src/libs/util/data-fetcher/src/lib/readers/wfs.ts +184 -128
  61. package/src/libs/util/data-fetcher/src/lib/utils.ts +0 -143
  62. package/src/libs/util/shared/src/index.ts +1 -0
  63. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  64. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  65. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  66. package/src/libs/util/shared/src/lib/utils/geojson.ts +8 -0
  67. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  68. package/tailwind.base.css +5 -0
  69. package/translations/de.json +15 -2
  70. package/translations/en.json +14 -1
  71. package/translations/es.json +13 -0
  72. package/translations/fr.json +15 -2
  73. package/translations/it.json +13 -0
  74. package/translations/nl.json +13 -0
  75. package/translations/pt.json +13 -0
  76. package/translations/sk.json +13 -0
  77. package/src/libs/util/data-fetcher/src/lib/readers/base-cache.ts +0 -14
@@ -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,7 +20,7 @@
20
20
  </div>
21
21
  }
22
22
  </div>
23
- <button class="h-6 w-6" data-cy="clearSelection">
23
+ <button class="h-6 w-6" data-test="dropdown-clear">
24
24
  @if (hasSelectedChoices && !overlayOpen) {
25
25
  <ng-icon
26
26
  class="shrink-0 opacity-40 mr-1.5 hover:opacity-80 transition-colors clear-btn"
@@ -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>