• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geonetwork / geonetwork-ui / 14263408054

04 Apr 2025 10:30AM UTC coverage: 84.275% (+2.5%) from 81.764%
14263408054

Pull #1193

github

web-flow
Merge d909283be into 4fa0a0afc
Pull Request #1193: Section subsection rework

1587 of 2126 branches covered (74.65%)

Branch coverage included in aggregate %.

5080 of 5785 relevant lines covered (87.81%)

10.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.1
/libs/ui/elements/src/lib/image-input/image-input.component.ts
1
import { CommonModule } from '@angular/common'
1✔
2
import { HttpClient } from '@angular/common/http'
1✔
3
import {
1✔
4
  ChangeDetectionStrategy,
5
  ChangeDetectorRef,
6
  Component,
7
  EventEmitter,
8
  Input,
9
  Output,
10
} from '@angular/core'
11
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'
1✔
12
import { marker } from '@biesbjerg/ngx-translate-extract-marker'
1✔
13
import {
1✔
14
  ButtonComponent,
15
  FilesDropDirective,
16
  TextInputComponent,
17
  UrlInputComponent,
18
} from '@geonetwork-ui/ui/inputs'
19
import { downgradeImage, megabytesToBytes } from '@geonetwork-ui/util/shared'
1✔
20
import {
1✔
21
  NgIconComponent,
22
  provideIcons,
23
  provideNgIconsConfig,
24
} from '@ng-icons/core'
25
import {
1✔
26
  iconoirBin,
27
  iconoirFramePlusIn,
28
  iconoirLink,
29
  iconoirMediaImage,
30
  iconoirMediaImageXmark,
31
  iconoirPlus,
32
} from '@ng-icons/iconoir'
33
import { TranslateModule } from '@ngx-translate/core'
1✔
34
import { firstValueFrom } from 'rxjs'
1✔
35
import { ImageOverlayPreviewComponent } from '../image-overlay-preview/image-overlay-preview.component'
1✔
36

37
@Component({
38
  selector: 'gn-ui-image-input',
39
  templateUrl: './image-input.component.html',
40
  styleUrls: ['./image-input.component.css'],
41
  changeDetection: ChangeDetectionStrategy.OnPush,
42
  standalone: true,
43
  imports: [
44
    CommonModule,
45
    ButtonComponent,
46
    FilesDropDirective,
47
    MatProgressSpinnerModule,
48
    TranslateModule,
49
    UrlInputComponent,
50
    TextInputComponent,
51
    NgIconComponent,
52
    ImageOverlayPreviewComponent,
53
  ],
54
  providers: [
55
    provideIcons({
56
      iconoirMediaImage,
57
      iconoirFramePlusIn,
58
      iconoirMediaImageXmark,
59
      iconoirBin,
60
      iconoirPlus,
61
      iconoirLink,
62
    }),
63
    provideNgIconsConfig({
64
      size: '1.5rem',
65
    }),
66
  ],
67
})
68
export class ImageInputComponent {
1✔
69
  private _altText?: string
70

71
  @Input() previewUrl?: string
72
  @Input()
73
  get altText(): string | undefined {
74
    return this._altText
×
75
  }
76
  set altText(value: string | undefined) {
77
    if (value !== 'KO' && this._altText === 'KO') {
4✔
78
      //This is a dataset rollback after upload error
79
      this.resetErrors()
2✔
80
    }
81
    this._altText = value
4✔
82
  }
83

84
  @Input() maxSizeMB: number
85
  @Input() uploadProgress?: number
86
  @Input() uploadError?: boolean
87
  @Input() disabled?: boolean = false
8✔
88
  @Output() fileChange: EventEmitter<File> = new EventEmitter()
8✔
89
  @Output() urlChange: EventEmitter<string> = new EventEmitter()
8✔
90
  @Output() uploadCancel: EventEmitter<void> = new EventEmitter()
8✔
91
  @Output() delete: EventEmitter<void> = new EventEmitter()
8✔
92
  @Output() altTextChange: EventEmitter<string> = new EventEmitter()
8✔
93

94
  dragFilesOver = false
8✔
95
  showUrlInput = false
8✔
96
  imageFileError = this.uploadError
8✔
97
  showAltTextInput = false
8✔
98
  pendingAltText: string
99

100
  get isUploadInProgress() {
101
    return this.uploadProgress !== undefined
48✔
102
  }
103

104
  constructor(
105
    private http: HttpClient,
8!
106
    private cd: ChangeDetectorRef
8✔
107
  ) {}
108

109
  getIsActionBlocked() {
110
    return this.isUploadInProgress || this.disabled
16✔
111
  }
112

113
  getPrimaryText() {
114
    if (this.imageFileError) {
8!
115
      return marker('input.image.uploadErrorLabel')
×
116
    }
117
    if (this.uploadProgress) {
8!
118
      return marker('input.image.uploadProgressLabel')
×
119
    }
120
    return marker('input.image.selectFileLabel')
8✔
121
  }
122

123
  getSecondaryText() {
124
    if (this.imageFileError) {
8!
125
      return '\u00A0' // (only to keep same spacing, next step is to handle "Retry")
×
126
    }
127
    if (this.uploadProgress) {
8!
128
      return marker('input.image.uploadProgressCancel')
×
129
    }
130
    return marker('input.image.dropFileLabel')
8✔
131
  }
132

133
  handleDragFilesOver(dragFilesOver: boolean) {
134
    if (!this.showUrlInput) {
×
135
      this.dragFilesOver = dragFilesOver
×
136
      this.cd.markForCheck()
×
137
    }
138
  }
139

140
  handleDropFiles(files: File[]) {
141
    this.resetErrors()
1✔
142
    const validFiles = this.filterTypeImage(files)
1✔
143
    if (validFiles.length > 0) {
1!
144
      this.showUrlInput = false
×
145
      this.resizeAndEmit(validFiles[0])
×
146
    } else {
147
      this.imageFileError = true
1✔
148
      this.handleAltTextChange('KO')
1✔
149
    }
150
  }
151

152
  handleFileInput(event: Event) {
153
    this.resetErrors()
×
154
    const inputFiles = Array.from((event.target as HTMLInputElement).files)
×
155
    const validFiles = this.filterTypeImage(inputFiles)
×
156
    if (validFiles.length > 0) {
×
157
      this.resizeAndEmit(validFiles[0])
×
158
    } else {
159
      this.imageFileError = true
×
160
      this.handleAltTextChange('KO')
×
161
    }
162
  }
163

164
  displayUrlInput() {
165
    this.uploadCancel.emit()
×
166
    this.showUrlInput = true
×
167
  }
168

169
  async downloadUrl(url: string) {
170
    this.resetErrors()
5✔
171
    const name = url.split('/').pop()
5✔
172
    try {
5✔
173
      const response = await firstValueFrom(
5✔
174
        this.http.head(url, { observe: 'response' })
175
      )
176
      if (
2✔
177
        response.headers.get('content-type')?.startsWith('image/') &&
4✔
178
        parseInt(response.headers.get('content-length')) <
179
          megabytesToBytes(this.maxSizeMB)
180
      ) {
181
        this.http.get(url, { responseType: 'blob' }).subscribe({
2✔
182
          next: (blob) => {
183
            this.cd.markForCheck()
1✔
184
            const file = new File([blob], name)
1✔
185
            this.fileChange.emit(file)
1✔
186
          },
187
          error: () => {
188
            this.imageFileError = true
1✔
189
            this.handleAltTextChange('KO')
1✔
190
            this.cd.markForCheck()
1✔
191
            this.urlChange.emit(url)
1✔
192
          },
193
        })
194
      }
195
    } catch {
196
      this.imageFileError = true
1✔
197
      this.handleAltTextChange('KO')
1✔
198
      this.cd.markForCheck()
1✔
199
      return
1✔
200
    }
201
  }
202

203
  handleSecondaryTextClick(event: Event) {
204
    if (this.uploadProgress) {
×
205
      this.handleCancelUpload()
×
206
      event.preventDefault()
×
207
    }
208
  }
209

210
  handleCancelUpload() {
211
    this.uploadCancel.emit()
×
212
  }
213

214
  handleDelete() {
215
    this.delete.emit()
×
216
  }
217

218
  resetErrors() {
219
    this.imageFileError = false
8✔
220
    this.uploadError = false
8✔
221
  }
222

223
  toggleAltTextInput() {
224
    this.showAltTextInput = !this.showAltTextInput
×
225
  }
226

227
  handleAltTextChange(altText: string) {
228
    this.altTextChange.emit(altText)
3✔
229
  }
230
  private filterTypeImage(files: File[]) {
231
    return files.filter((file) => {
2✔
232
      return file.type.startsWith('image/')
3✔
233
    })
234
  }
235

236
  private resizeAndEmit(imageToResize: File) {
237
    const maxSizeBytes = megabytesToBytes(this.maxSizeMB)
×
238
    downgradeImage(imageToResize, maxSizeBytes).then((resizedImage) => {
×
239
      const fileToEmit = new File([resizedImage], imageToResize.name)
×
240
      this.fileChange.emit(fileToEmit)
×
241
    })
242
  }
243
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc