geonetwork-ui 2.11.0-dev.e430b2fe8 → 2.11.0-dev.fb3fea50c

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 (47) hide show
  1. package/fesm2022/geonetwork-ui.mjs +648 -201
  2. package/fesm2022/geonetwork-ui.mjs.map +1 -1
  3. package/index.d.ts +142 -31
  4. package/index.d.ts.map +1 -1
  5. package/package.json +2 -1
  6. package/src/libs/api/repository/src/lib/gn4/elasticsearch/elasticsearch.service.ts +76 -34
  7. package/src/libs/common/domain/src/lib/model/search/filter.model.ts +13 -1
  8. package/src/libs/feature/map/src/lib/add-layer-from-file/add-layer-from-file.component.html +1 -0
  9. package/src/libs/feature/notifications/src/lib/notifications.service.ts +6 -4
  10. package/src/libs/feature/router/src/lib/default/state/query-params.utils.ts +1 -1
  11. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.html +12 -4
  12. package/src/libs/feature/search/src/lib/filter-dropdown/filter-dropdown.component.ts +58 -19
  13. package/src/libs/feature/search/src/lib/search-filters-summary-item/search-filters-summary-item.component.ts +18 -8
  14. package/src/libs/feature/search/src/lib/utils/service/fields.service.ts +9 -1
  15. package/src/libs/feature/search/src/lib/utils/service/fields.ts +43 -2
  16. package/src/libs/ui/inputs/src/index.ts +2 -0
  17. package/src/libs/ui/inputs/src/lib/date-adapter.providers.ts +30 -0
  18. package/src/libs/ui/inputs/src/lib/date-picker/date-picker.component.ts +3 -24
  19. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.css +37 -3
  20. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.html +135 -16
  21. package/src/libs/ui/inputs/src/lib/date-range-dropdown/date-range-dropdown.component.ts +166 -29
  22. package/src/libs/ui/inputs/src/lib/date-range-picker/date-range-picker.component.ts +3 -7
  23. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.css +1 -0
  24. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.html +20 -7
  25. package/src/libs/ui/inputs/src/lib/drag-and-drop-file-input/drag-and-drop-file-input.component.ts +46 -5
  26. package/src/libs/ui/inputs/src/lib/dropdown-multiselect/dropdown-multiselect.component.html +1 -1
  27. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.css +0 -0
  28. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.html +111 -0
  29. package/src/libs/ui/inputs/src/lib/spatial-extent-dropdown/spatial-extent-dropdown.component.ts +215 -0
  30. package/src/libs/ui/map/src/lib/components/map-container/map-container.component.ts +2 -0
  31. package/src/libs/util/app-config/src/lib/app-config.ts +3 -0
  32. package/src/libs/util/app-config/src/lib/model.ts +1 -0
  33. package/src/libs/util/shared/src/index.ts +1 -0
  34. package/src/libs/util/shared/src/lib/autofocus.directive.ts +26 -0
  35. package/src/libs/util/shared/src/lib/services/date.service.ts +3 -3
  36. package/src/libs/util/shared/src/lib/utils/file.ts +15 -0
  37. package/src/libs/util/shared/src/lib/utils/geojson.ts +8 -0
  38. package/src/libs/util/shared/src/lib/utils/index.ts +1 -0
  39. package/tailwind.base.css +5 -0
  40. package/translations/de.json +16 -2
  41. package/translations/en.json +15 -1
  42. package/translations/es.json +14 -0
  43. package/translations/fr.json +16 -2
  44. package/translations/it.json +14 -0
  45. package/translations/nl.json +14 -0
  46. package/translations/pt.json +14 -0
  47. package/translations/sk.json +14 -0
@@ -2,15 +2,9 @@ import {
2
2
  ChangeDetectionStrategy,
3
3
  Component,
4
4
  EventEmitter,
5
- inject,
6
5
  Input,
7
6
  Output,
8
7
  } from '@angular/core'
9
- import {
10
- DateAdapter,
11
- MAT_DATE_LOCALE,
12
- MatNativeDateModule,
13
- } from '@angular/material/core'
14
8
  import { MatDatepickerModule } from '@angular/material/datepicker'
15
9
  import { ButtonComponent } from '../button/button.component'
16
10
  import {
@@ -19,7 +13,7 @@ import {
19
13
  provideNgIconsConfig,
20
14
  } from '@ng-icons/core'
21
15
  import { iconoirCalendar } from '@ng-icons/iconoir'
22
- import { TranslateService } from '@ngx-translate/core'
16
+ import { provideLocalizedDateAdapter } from '../date-adapter.providers'
23
17
 
24
18
  @Component({
25
19
  selector: 'gn-ui-date-picker',
@@ -27,31 +21,16 @@ import { TranslateService } from '@ngx-translate/core'
27
21
  styleUrls: ['./date-picker.component.css'],
28
22
  changeDetection: ChangeDetectionStrategy.OnPush,
29
23
  standalone: true,
30
- imports: [
31
- MatNativeDateModule,
32
- MatDatepickerModule,
33
- ButtonComponent,
34
- NgIconComponent,
35
- ],
24
+ imports: [MatDatepickerModule, ButtonComponent, NgIconComponent],
36
25
  providers: [
37
26
  provideIcons({ iconoirCalendar }),
38
27
  provideNgIconsConfig({
39
28
  size: '1.5rem',
40
29
  }),
41
- {
42
- provide: MAT_DATE_LOCALE,
43
- useFactory: (locale: string) => locale,
44
- },
30
+ provideLocalizedDateAdapter(),
45
31
  ],
46
32
  })
47
33
  export class DatePickerComponent {
48
- private dateAdapter = inject<DateAdapter<Date>>(DateAdapter)
49
- private translate = inject(TranslateService)
50
-
51
34
  @Input() date: Date
52
35
  @Output() dateChange = new EventEmitter<Date>()
53
-
54
- constructor() {
55
- this.dateAdapter.setLocale(this.translate.getCurrentLang())
56
- }
57
36
  }
@@ -1,5 +1,39 @@
1
- :host {
2
- .mat-date-range-input-container {
3
- display: none;
1
+ mat-calendar {
2
+ width: 100%;
3
+ }
4
+
5
+ .calendar {
6
+ display: grid;
7
+ grid-template-rows: 1fr;
8
+ }
9
+
10
+ .calendar > mat-calendar {
11
+ min-height: 0;
12
+ overflow: hidden;
13
+ }
14
+
15
+ .calendar.expand {
16
+ animation: open 200ms ease-out;
17
+ }
18
+
19
+ .calendar.collapse {
20
+ animation: open 200ms ease-in reverse;
21
+ }
22
+
23
+ @keyframes open {
24
+ from {
25
+ grid-template-rows: 0fr;
26
+ opacity: 0;
27
+ }
28
+ to {
29
+ grid-template-rows: 1fr;
30
+ opacity: 1;
31
+ }
32
+ }
33
+
34
+ @media (prefers-reduced-motion: reduce) {
35
+ .calendar.expand,
36
+ .calendar.collapse {
37
+ animation-duration: 1ms;
4
38
  }
5
39
  }
@@ -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"